diff --git a/packages/mgt-chat/src/statefulClient/GraphNotificationClient.ts b/packages/mgt-chat/src/statefulClient/GraphNotificationClient.ts index 054bf9742c..d328cc2dd9 100644 --- a/packages/mgt-chat/src/statefulClient/GraphNotificationClient.ts +++ b/packages/mgt-chat/src/statefulClient/GraphNotificationClient.ts @@ -6,7 +6,13 @@ */ import { BetaGraph, IGraph, Providers, createFromProvider, error, log } from '@microsoft/mgt-element'; -import { HubConnection, HubConnectionBuilder, IHttpConnectionOptions, LogLevel } from '@microsoft/signalr'; +import { + HubConnection, + HubConnectionBuilder, + HubConnectionState, + IHttpConnectionOptions, + LogLevel +} from '@microsoft/signalr'; import { ThreadEventEmitter } from './ThreadEventEmitter'; import type { Entity, @@ -19,13 +25,13 @@ import { GraphConfig } from './GraphConfig'; import { SubscriptionsCache } from './Caching/SubscriptionCache'; import { Timer } from '../utils/Timer'; import { getOrGenerateGroupId } from './getOrGenerateGroupId'; +import { v4 as uuid } from 'uuid'; export const appSettings = { defaultSubscriptionLifetimeInMinutes: 10, renewalThreshold: 75, // The number of seconds before subscription expires it will be renewed - renewalTimerInterval: 20, // The number of seconds between executions of the renewal timer - removalThreshold: 1 * 60, // number of seconds after the last update of a subscription to consider in inactive - removalTimerInterval: 1 * 60, // the number of seconds between executions of the timer to remove inactive subscriptions + renewalTimerInterval: 3, // The number of seconds between executions of the renewal timer + fastRenewalInterval: 500, // The number of milliseconds between executions of the fast renewal timer useCanary: GraphConfig.useCanary }; @@ -51,12 +57,23 @@ const isMembershipNotification = (o: Notification): o is Notification { log(`Reconnected. ConnectionId: ${connectionId || 'undefined'}`); - // void this.renewChatSubscriptions(); }; private readonly receiveNotificationMessage = (message: string) => { if (typeof message !== 'string') throw new Error('Expected string from receivenotificationmessageasync'); + const ackMessage: unknown = { StatusCode: '200' }; const notification: ReceivedNotification = JSON.parse(message) as ReceivedNotification; - log('received notification message', notification); + // only process notifications for the current chat's subscriptions + if (this.subscriptionIds.length > 0 && !this.subscriptionIds.includes(notification.subscriptionId)) { + log('Received chat notification message for a different chat subscription', notification); + return ackMessage; + } + + log('received chat notification message', notification); const emitter: ThreadEventEmitter | undefined = this.emitter; if (!notification.resourceData) throw new Error('Message did not contain resourceData'); if (isMessageNotification(notification)) { @@ -120,7 +143,6 @@ export class GraphNotificationClient { this.processChatPropertiesNotification(notification, emitter); } // Need to return a status code string of 200 so that graph knows the message was received and doesn't re-send the notification - const ackMessage: unknown = { StatusCode: '200' }; return GraphConfig.ackAsString ? JSON.stringify(ackMessage) : ackMessage; }; @@ -173,116 +195,78 @@ export class GraphNotificationClient { } } - private readonly cacheSubscription = async (subscriptionRecord: Subscription): Promise => { + private readonly cacheSubscription = async (chatId: string, subscriptionRecord: Subscription): Promise => { log(subscriptionRecord); - - await this.subscriptionCache.cacheSubscription(this.chatId, subscriptionRecord); - - // only start timer once. undefined for renewalInterval is semaphore it has stopped. - if (this.renewalTimeout === undefined) this.startRenewalTimer(); + await this.subscriptionCache.cacheSubscription(chatId, subscriptionRecord); }; - private async subscribeToResource(resourcePath: string, changeTypes: ChangeTypes[]) { + private async subscribeToResource(resourcePath: string, groupId: string, changeTypes: ChangeTypes[]) { // build subscription request const expirationDateTime = new Date( new Date().getTime() + appSettings.defaultSubscriptionLifetimeInMinutes * 60 * 1000 ).toISOString(); const subscriptionDefinition: Subscription = { changeType: changeTypes.join(','), - notificationUrl: `${GraphConfig.webSocketsPrefix}?groupId=${getOrGenerateGroupId(this.chatId)}`, + notificationUrl: `${GraphConfig.webSocketsPrefix}?groupId=${groupId}`, resource: resourcePath, expirationDateTime, includeResourceData: true, clientState: 'wsssecret' }; - log('subscribing to changes for ' + resourcePath); + log(`subscribing to changes for ${resourcePath}`); const subscriptionEndpoint = GraphConfig.subscriptionEndpoint; const subscriptionGraph = this.subscriptionGraph; + if (!subscriptionGraph) return; // send subscription POST to Graph - const subscription: Subscription = (await subscriptionGraph - .api(subscriptionEndpoint) - .post(subscriptionDefinition)) as Subscription; - if (!subscription?.notificationUrl) throw new Error('Subscription not created'); + let subscription: Subscription; + subscription = (await subscriptionGraph.api(subscriptionEndpoint).post(subscriptionDefinition)) as Subscription; + if (!subscription?.notificationUrl) { + throw new Error('Subscription not created'); + } log(subscription); - const awaits: Promise[] = []; - // Cache the subscription in storage for re-hydration on page refreshes - awaits.push(this.cacheSubscription(subscription)); - - // create a connection to the web socket if one does not exist - if (!this.connection) awaits.push(this.createSignalRConnection(subscription.notificationUrl)); - log('Invoked CreateSubscription'); - return Promise.all(awaits); + return subscription; } - private readonly startRenewalTimer = () => { - if (this.renewalTimeout) this.timer.clearTimeout(this.renewalTimeout); - this.renewalTimeout = this.timer.setTimeout(this.syncRenewalTimerWrapper, appSettings.renewalTimerInterval * 1000); - log(`Start renewal timer . Id: ${this.renewalTimeout}`); - }; - - private readonly syncRenewalTimerWrapper = () => void this.renewalTimer(); - - private readonly renewalTimer = async () => { - log(`running subscription renewal timer for chatId: ${this.chatId} sessionId: ${this.sessionId}`); - const subscriptions = (await this.subscriptionCache.loadSubscriptions(this.chatId))?.subscriptions || []; - if (subscriptions.length === 0) { - log(`No subscriptions found in session state. Stop renewal timer ${this.renewalTimeout}.`); - if (this.renewalTimeout) this.timer.clearTimeout(this.renewalTimeout); - return; - } + public renewSubscriptions = async (chatId: string) => { + log(`Renewing Graph subscription for ChatId. RenewalCount: ${this.renewalCount}.`); - for (const subscription of subscriptions) { - if (!subscription.expirationDateTime) continue; - const expirationTime = new Date(subscription.expirationDateTime); - const now = new Date(); - const diff = Math.round((expirationTime.getTime() - now.getTime()) / 1000); - - if (diff <= appSettings.renewalThreshold) { - this.renewalCount++; - log(`Renewing Graph subscription. RenewalCount: ${this.renewalCount}`); - // stop interval to prevent new invokes until refresh is ready. - if (this.renewalTimeout) this.timer.clearTimeout(this.renewalTimeout); - this.renewalTimeout = undefined; - await this.renewChatSubscriptions(); - // There is one subscription that need expiration, all subscriptions will be renewed - break; - } - } - this.renewalTimeout = this.timer.setTimeout(this.syncRenewalTimerWrapper, appSettings.renewalTimerInterval * 1000); - }; - - public renewChatSubscriptions = async () => { - const expirationTime = new Date( + const newExpirationTime = new Date( new Date().getTime() + appSettings.defaultSubscriptionLifetimeInMinutes * 60 * 1000 - ); + ).toISOString(); - const subscriptionCache = await this.subscriptionCache.loadSubscriptions(this.chatId); + let subscriptions = await this.getSubscriptions(chatId); const awaits: Promise[] = []; - for (const subscription of subscriptionCache?.subscriptions || []) { - if (!subscription.id) continue; + for (const subscription of subscriptions || []) { // the renewSubscription method caches the updated subscription to track the new expiration time - awaits.push(this.renewSubscription(subscription.id, expirationTime.toISOString())); + awaits.push(this.renewSubscription(chatId, subscription.id!, newExpirationTime)); log(`Invoked RenewSubscription ${subscription.id}`); + this.renewalCount++; } await Promise.all(awaits); - if (!this.renewalTimeout) { - this.renewalTimeout = this.timer.setTimeout( - this.syncRenewalTimerWrapper, - appSettings.renewalTimerInterval * 1000 - ); - } }; - public renewSubscription = async (subscriptionId: string, expirationDateTime: string): Promise => { + private renewSubscription = async ( + chatId: string, + subscriptionId: string, + expirationDateTime: string + ): Promise => { // PATCH /subscriptions/{id} - const renewedSubscription = (await this.graph?.api(`${GraphConfig.subscriptionEndpoint}/${subscriptionId}`).patch({ - expirationDateTime - })) as Subscription | undefined; - if (renewedSubscription) return this.cacheSubscription(renewedSubscription); + try { + const renewedSubscription = (await this.graph + ?.api(`${GraphConfig.subscriptionEndpoint}/${subscriptionId}`) + .patch({ + expirationDateTime + })) as Subscription | undefined; + if (renewedSubscription) { + return this.cacheSubscription(chatId, renewedSubscription); + } + } catch (e) { + return Promise.reject(e); + } }; public async createSignalRConnection(notificationUrl: string) { @@ -290,6 +274,8 @@ export class GraphNotificationClient { accessTokenFactory: this.getToken, withCredentials: false }; + // log the notification url and session id + log(`Creating SignalR connection for notification url: ${notificationUrl} with session id: ${this.sessionId}`); const connection = new HubConnectionBuilder() .withUrl(GraphConfig.adjustNotificationUrl(notificationUrl, this.sessionId), connectionOptions) .withAutomaticReconnect() @@ -311,98 +297,196 @@ export class GraphNotificationClient { } } - private async deleteSubscription(id: string) { + private createSubscriptions = async (chatId: string) => { + const promises: Promise[] = []; + const groupId = getOrGenerateGroupId(chatId); + promises.push(this.subscribeToResource(`/chats/${chatId}/messages`, groupId, ['created', 'updated', 'deleted'])); + promises.push(this.subscribeToResource(`/chats/${chatId}/members`, groupId, ['created', 'deleted'])); + promises.push(this.subscribeToResource(`/chats/${chatId}`, groupId, ['updated', 'deleted'])); + const results = await Promise.all(promises); + + // Cache the subscriptions in storage for re-hydration on page refreshes + const awaits: Promise[] = []; + const subscriptions: Subscription[] = (results as (Subscription | undefined)[]).filter(Boolean) as Subscription[]; + for (let subscription of subscriptions) { + awaits.push(this.cacheSubscription(chatId, subscription)); + } + await Promise.all(awaits); + return subscriptions; + }; + + private async deleteCachedSubscriptions(chatId: string) { try { - await this.graph?.api(`${GraphConfig.subscriptionEndpoint}/${id}`).delete(); + log('Removing all chat subscriptions from cache for chatId:', chatId); + await this.subscriptionCache.deleteCachedSubscriptions(chatId); + log('Successfully removed all chat subscriptions from cache.'); } catch (e) { - error(e); + error(`Failed to remove chat subscription for ${chatId} from cache.`, e); } } - private async removeSubscriptions(subscriptions: Subscription[]): Promise { - const tasks: Promise[] = []; - for (const s of subscriptions) { - // if there is no id or the subscription is expired, skip - if (!s.id || (s.expirationDateTime && new Date(s.expirationDateTime) <= new Date())) continue; - tasks.push(this.deleteSubscription(s.id)); + private async getSubscriptions(chatId: string): Promise { + const subscriptions = (await this.subscriptionCache.loadSubscriptions(chatId))?.subscriptions || []; + return subscriptions.length > 0 ? subscriptions : undefined; + } + + private trySwitchToConnected() { + if (this.wasConnected !== true) { + log('The user will receive notifications from the chat subscriptions.'); + this.wasConnected = true; + this.emitter?.connected(); } - return Promise.all(tasks); } - private startCleanupTimer() { - this.cleanupTimeout = this.timer.setTimeout(this.cleanupTimerSync, appSettings.removalTimerInterval * 1000); + private trySwitchToDisconnected(ignoreIfUndefined = false) { + if (ignoreIfUndefined && this.wasConnected === undefined) return; + if (this.wasConnected !== false) { + log('The user will NOT receive notifications from the chat subscriptions.'); + this.wasConnected = false; + this.emitter?.disconnected(); + } } - private readonly cleanupTimerSync = () => { - void this.cleanupTimer(); + private readonly renewalSync = () => { + void this.renewal(); }; - private readonly cleanupTimer = async () => { - log(`running cleanup timer`); - const offset = Math.min( - appSettings.removalThreshold * 1000, - appSettings.defaultSubscriptionLifetimeInMinutes * 60 * 1000 - ); - const threshold = new Date(new Date().getTime() - offset).toISOString(); - const inactiveSubs = await this.subscriptionCache.loadInactiveSubscriptions(threshold); - let tasks: Promise[] = []; - for (const inactive of inactiveSubs) { - tasks.push(this.removeSubscriptions(inactive.subscriptions)); - } - await Promise.all(tasks); - tasks = []; - for (const inactive of inactiveSubs) { - tasks.push(this.subscriptionCache.deleteCachedSubscriptions(inactive.chatId)); + private readonly renewal = async () => { + let nextRenewalTimeInSec = appSettings.renewalTimerInterval; + try { + const chatId = this.chatId; + + // this allows us to renew on chatId change much faster than the normal renewal interval + const timeElapsed = new Date().getTime() - this.lastRenewalTime.getTime(); + if (timeElapsed < appSettings.renewalTimerInterval * 1000 && chatId === this.previousChatId) { + this.lastRenewalTime = new Date(); + return; + } + + // if there are current subscriptions for this chat id... + let subscriptions = await this.getSubscriptions(chatId); + if (subscriptions) { + // attempt a renewal if necessary + const subscriptionIds = subscriptions?.map(s => s?.id).filter(Boolean); + try { + for (let subscription of subscriptions) { + const expirationTime = new Date(subscription.expirationDateTime!); + const diff = Math.round((expirationTime.getTime() - new Date().getTime()) / 1000); + if (diff <= 0) { + log(`Renewing chat subscription ${subscription.id} that has already expired...`); + this.trySwitchToDisconnected(true); + await this.renewSubscriptions(chatId); + log(`Successfully renewed chat subscription ${subscription.id}.`); + break; + } else if (diff <= appSettings.renewalThreshold) { + log(`Renewing chat subscription ${subscription.id} that will expire in ${diff} seconds...`); + await this.renewSubscriptions(chatId); + log(`Successfully renewed chat subscription ${subscription.id}.`); + break; + } + } + } catch (e) { + error(`Failed to renew chat subscriptions for ${subscriptionIds}.`, e); + await this.deleteCachedSubscriptions(chatId); + subscriptions = undefined; + } + } + + // if there are no subscriptions, try to create them + if (!subscriptions) { + try { + this.trySwitchToDisconnected(true); + subscriptions = await this.createSubscriptions(chatId); + } catch (e) { + const err = e as { statusCode?: number; message: string }; + if (err.statusCode === 403 && err.message.indexOf('has reached its limit') > 0) { + // if the limit is reached, back-off (NOTE: this should probably be a 429) + nextRenewalTimeInSec = appSettings.renewalTimerInterval * 3; + throw new Error( + `Failed to create new chat subscriptions due to a limitation; retrying in ${nextRenewalTimeInSec} seconds: ${err.message}.` + ); + } else if (err.statusCode === 403 || err.statusCode === 402) { + // permanent error, stop renewal + error('Failed to create new chat subscriptions due to a permanent condition; stopping renewals.', e); + nextRenewalTimeInSec = -1; + return; // exit and don't reschedule the next renewal + } else { + // transient error, retry + throw new Error( + `Failed to create new chat subscriptions due to a transient condition; retrying in ${nextRenewalTimeInSec} seconds: ${err.message}.` + ); + } + } + } + + // create or reconnect the SignalR connection + // notificationUrl comes in the form of websockets:https://graph.microsoft.com/beta/subscriptions/notificationChannel/websockets/?groupid=&sessionid=default + // if changes, we need to create a new connection + const subscriptionIds = subscriptions.map(s => s.id!); + if (this.connection?.state === HubConnectionState.Connected) { + await this.connection?.send('ping'); // ensure the connection is still alive + } + if (!this.connection) { + log(`Creating a new SignalR connection for chat subscriptions: ${subscriptionIds}...`); + this.trySwitchToDisconnected(true); + this.lastNotificationUrl = subscriptions![0]?.notificationUrl!; + await this.createSignalRConnection(subscriptions![0]?.notificationUrl!); + log(`Successfully created a new SignalR connection for chat subscriptions: ${subscriptionIds}`); + } else if (this.connection.state !== HubConnectionState.Connected) { + log(`Reconnecting SignalR connection for chat subscriptions: ${subscriptionIds}...`); + this.trySwitchToDisconnected(true); + await this.connection.start(); + log(`Successfully reconnected SignalR connection for chat subscriptions: ${subscriptionIds}`); + } else if (this.lastNotificationUrl !== subscriptions![0]?.notificationUrl) { + log(`Updating SignalR connection for chat subscriptions: ${subscriptionIds} due to new notification URL...`); + this.trySwitchToDisconnected(true); + await this.closeSignalRConnection(); + this.lastNotificationUrl = subscriptions![0]?.notificationUrl!; + await this.createSignalRConnection(subscriptions![0]?.notificationUrl!); + log(`Successfully updated SignalR connection for chat subscriptions: ${subscriptionIds}`); + } + + // emit the new connection event if necessary + this.trySwitchToConnected(); + // set if renewal was successful + this.lastRenewalTime = new Date(); + this.previousChatId = chatId; + this.subscriptionIds = subscriptionIds; + } catch (e) { + error('Error in chat subscription connection process.', e); + this.trySwitchToDisconnected(); + this?.emitter.graphNotificationClientError(e as Error); + } finally { + if (nextRenewalTimeInSec >= 0) { + this.renewalTimeout = this.timer.setTimeout( + 'renewal:' + this.instanceId, + this.renewalSync, + nextRenewalTimeInSec * 1000 + ); + } } - this.startCleanupTimer(); }; + /** + * Closes the SignalR connection + */ public async closeSignalRConnection() { // stop the connection and set it to undefined so it will reconnect when next subscription is created. await this.connection?.stop(); this.connection = undefined; } - private async unsubscribeFromChatNotifications(chatId: string) { - await this.closeSignalRConnection(); - const cacheData = await this.subscriptionCache.loadSubscriptions(chatId); - if (cacheData) { - await Promise.all([ - this.removeSubscriptions(cacheData.subscriptions), - this.subscriptionCache.deleteCachedSubscriptions(chatId) - ]); - } - } - - public async subscribeToChatNotifications(chatId: string, sessionId: string) { + /** + * Subscribes to chat notifications for the given chatId + * @param chatId chat id to subscribe to + */ + public async subscribeToChatNotifications(chatId: string) { + log(`Subscribing to chat notifications for chatId: ${chatId}`); + this.wasConnected = undefined; this.chatId = chatId; - this.sessionId = sessionId; - // MGT uses a per-user cache, so no concerns of loading the cached data for another user. - const cacheData = await this.subscriptionCache.loadSubscriptions(chatId); - if (cacheData) { - // check subscription validity & renew if all still valid otherwise recreate - const someExpired = cacheData.subscriptions.some( - s => s.expirationDateTime && new Date(s.expirationDateTime) <= new Date() - ); - // for a given user + app + chatId + sessionId they only get one websocket and receive all notifications via that websocket. - const webSocketUrl = cacheData.subscriptions.find(s => s.notificationUrl)?.notificationUrl; - if (!someExpired && webSocketUrl) { - // if we have a websocket url and all the subscriptions are valid, we can reuse the websocket and return before recreating subscriptions. - await this.createSignalRConnection(webSocketUrl); - await this.renewChatSubscriptions(); - return; - } else if (someExpired) { - // if some are expired, remove them and continue to recreate the subscription - await this.removeSubscriptions(cacheData.subscriptions); - } - await this.subscriptionCache.deleteCachedSubscriptions(chatId); + // start the renewal timer only once + if (!this.renewalTimeout) { + this.renewalTimeout = this.timer.setTimeout('renewal:' + this.instanceId, this.renewalSync, 0); } - const promises: Promise[] = []; - promises.push(this.subscribeToResource(`/chats/${chatId}/messages`, ['created', 'updated', 'deleted'])); - promises.push(this.subscribeToResource(`/chats/${chatId}/members`, ['created', 'deleted'])); - promises.push(this.subscribeToResource(`/chats/${chatId}`, ['updated', 'deleted'])); - await Promise.all(promises).catch((e: Error) => { - this?.emitter.graphNotificationClientError(e); - }); } } diff --git a/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts b/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts index dddf3a7bac..d0498639e7 100644 --- a/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts +++ b/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts @@ -405,16 +405,13 @@ class StatefulGraphChatClient extends BaseStatefulClient { return; } this._chatId = value; - if (this._chatId && this._sessionId) { + if (this._chatId) { void this.updateFollowedChat(); } } - private _sessionId: string | undefined; - - public subscribeToChat(chatId: string, sessionId: string) { - if (chatId && sessionId) { - this._sessionId = sessionId; + public subscribeToChat(chatId: string) { + if (chatId) { this.chatId = chatId; } } @@ -434,7 +431,7 @@ class StatefulGraphChatClient extends BaseStatefulClient { */ private async updateFollowedChat() { // avoid subscribing to a resource with an empty chatId - if (this.chatId && this._sessionId) { + if (this.chatId) { // reset state to initial this.notifyStateChange((draft: GraphChatClient) => { draft.status = 'initial'; @@ -456,7 +453,7 @@ class StatefulGraphChatClient extends BaseStatefulClient { // subscribing to notifications will trigger the chatMessageNotificationsSubscribed event // this client will then load the chat and messages when that event listener is called if (this._notificationClient) { - tasks.push(this._notificationClient.subscribeToChatNotifications(this._chatId, this._sessionId)); + tasks.push(this._notificationClient.subscribeToChatNotifications(this._chatId)); await Promise.all(tasks); } } catch (e) { diff --git a/packages/mgt-chat/src/statefulClient/ThreadEventEmitter.ts b/packages/mgt-chat/src/statefulClient/ThreadEventEmitter.ts index fcaf8a2193..60db0e537a 100644 --- a/packages/mgt-chat/src/statefulClient/ThreadEventEmitter.ts +++ b/packages/mgt-chat/src/statefulClient/ThreadEventEmitter.ts @@ -66,6 +66,12 @@ export class ThreadEventEmitter { notificationsSubscribedForResource(resouce: string) { this.emitter.emit('notificationsSubscribedForResource', resouce); } + disconnected() { + this.emitter.emit('disconnected'); + } + connected() { + this.emitter.emit('connected'); + } graphNotificationClientError(error: Error) { this.emitter.emit('graphNotificationClientError', error); } diff --git a/packages/mgt-chat/src/statefulClient/useGraphChatClient.ts b/packages/mgt-chat/src/statefulClient/useGraphChatClient.ts index e7bb49688b..f91c988843 100644 --- a/packages/mgt-chat/src/statefulClient/useGraphChatClient.ts +++ b/packages/mgt-chat/src/statefulClient/useGraphChatClient.ts @@ -10,32 +10,21 @@ import { v4 as uuid } from 'uuid'; import { StatefulGraphChatClient } from './StatefulGraphChatClient'; import { log } from '@microsoft/mgt-element'; -/** - * Provides a stable sessionId for the lifetime of the browser tab. - * @returns a string that is either read from session storage or generated and placed in session storage - */ -const useSessionId = (): string => { - const [sessionId] = useState(() => uuid()); - - return sessionId; -}; - /** * Custom hook to abstract the creation of a stateful graph chat client. * @param {string} chatId the current chatId to be rendered * @returns {StatefulGraphChatClient} a stateful graph chat client that is subscribed to the given chatId */ export const useGraphChatClient = (chatId: string): StatefulGraphChatClient => { - const sessionId = useSessionId(); const [chatClient] = useState(() => new StatefulGraphChatClient()); // when chatId or sessionId changes this effect subscribes or unsubscribes // the component to/from web socket based notifications for the given chatId useEffect(() => { // we must have both a chatId & sessionId to subscribe. - if (chatId && sessionId) chatClient.subscribeToChat(chatId, sessionId); + if (chatId) chatClient.subscribeToChat(chatId); else chatClient.setStatus('no chat id'); - }, [chatId, sessionId, chatClient]); + }, [chatId, chatClient]); // Returns a cleanup function to call tearDown on the chatClient // This allows us to clean up when the consuming component is being unmounted from the DOM diff --git a/packages/mgt-chat/src/utils/Timer.ts b/packages/mgt-chat/src/utils/Timer.ts index bb3c9b86a0..0ddf3745f2 100644 --- a/packages/mgt-chat/src/utils/Timer.ts +++ b/packages/mgt-chat/src/utils/Timer.ts @@ -5,7 +5,6 @@ * ------------------------------------------------------------------------------------------- */ -import { v4 as uuid } from 'uuid'; import { TimerWork } from './timerWorker'; export interface Work { @@ -21,10 +20,12 @@ export class Timer { this.worker.port.onmessage = this.onMessage; } - public setTimeout(callback: () => void, delay: number): string { + // NOTE: this.work was increasing in size every time there was a new timeout because id was uuid() + // but clearTimeout was only being called on teardown. + public setTimeout(id: string, callback: () => void, delay: number): string { const timeoutWork: TimerWork = { type: 'setTimeout', - id: uuid(), + id, delay }; @@ -46,6 +47,8 @@ export class Timer { } } + /* + // NOTE: setInterval is not used in the library, but it is here for reference public setInterval(callback: () => void, delay: number): string { const intervalWork: TimerWork = { type: 'setInterval', @@ -70,6 +73,7 @@ export class Timer { this.work.delete(id); } } + */ private readonly onMessage = (event: MessageEvent): void => { const intervalWork = event.data;