The solution I'm currently developing requires me to handle the token expiration of Direct Line. Often users of the chat leave it open for quite some time as they are required to take a photo and upload it. During this time the chat might be left there for longer periods than 30 minutes and the Direct Line session expires, leaving users with unresponsive UI once they return.
So to get a glimpse of what I'm dealing with here is the chatStoreMiddleware I initially used to pass into WebChat:
import ReactWebChat from 'botframework-webchat';
import { createStore } from 'botframework-webchat';
enum DirectLineConnectionStatus {
Uninitialized,
Connecting,
Online,
ExpiredToken,
FailedToConnect,
Ended,
}
export const chatStoreMiddleware =
({ getState, dispatch }: Store): any =>
(next: Dispatch<AnyAction>) =>
(action: AnyAction): AnyAction | null =>
{
switch (action.type) {
case 'DIRECT_LINE/CONNECTION_STATUS_UPDATE': {
if (
action.payload &&
action.payload.connectionStatus ===
DirectLineConnectionStatus.Online
) {
handleConnectionOnline();
}
break;
}
case 'DIRECT_LINE/CONNECT_FULFILLED': {
handleConnectFulfilled(action);
break;
}
// other cases omitted
}
return next(action);
};
const chatStore = createStore(null, chatStoreMiddleware);
<ReactWebChat
store={chatStore}
// other props omitted
/>
However, as it can be seen in WebChat connectivityStatus.js reducers, they not expose expired token connection status.
So I kept digging and stumbled upon WebChat connectionStatusUpdate.js action where it states that 'DIRECT_LINE/CONNECTION_STATUS_UPDATE' action is obsolete and it only dispatches online status.
Alright, I decided to try my luck with WebChat updateConnectionStatus.js action like so
import ReactWebChat from 'botframework-webchat';
import { createStore } from 'botframework-webchat';
export const chatStoreMiddleware =
({ getState, dispatch }: Store): any =>
(next: Dispatch<AnyAction>) =>
(action: AnyAction): AnyAction | null =>
{
switch (action.type) {
/*
case 'DIRECT_LINE/CONNECTION_STATUS_UPDATE': {
if (
action.payload &&
action.payload.connectionStatus ===
DirectLineConnectionStatus.Online
) {
handleConnectionOnline();
}
break;
}
*/
case 'DIRECT_LINE/UPDATE_CONNECTION_STATUS': {
if (
action.payload &&
action.payload.connectionStatus ===
DirectLineConnectionStatus.ExpiredToken
) {
handleExpiredToken();
}
if (
action.payload &&
action.payload.connectionStatus ===
DirectLineConnectionStatus.Online
) {
handleConnectionOnline();
}
break;
}
case 'DIRECT_LINE/CONNECT_FULFILLED': {
handleConnectFulfilled(action);
break;
}
// other cases omitted
}
return next(action);
};
const chatStore = createStore(null, chatStoreMiddleware);
<ReactWebChat
store={chatStore}
// other props omitted
/>
Now when I try to launch the chat - it just loads forever. 馃槼
Ok, let's console.log things:
// code before omitted
case 'DIRECT_LINE/UPDATE_CONNECTION_STATUS': {
console.log('\n>>> UPDATE_CONNECTION_STATUS');
console.log('TYPE:', action.type);
console.log('PAYLOAD:', action.payload);
// handle update connection status
break;
case 'DIRECT_LINE/CONNECT_FULFILLED': {
console.log('\n>>> CONNECT_FULFILLED');
console.log('TYPE:', action.type);
console.log('PAYLOAD:', action.payload);
// handle connect fulfilled
break;
}
// code after omitted
10:19:32.056 chatStoreMiddleware.ts:64 >>> UPDATE_CONNECTION_STATUS
10:19:32.057 chatStoreMiddleware.ts:65 TYPE: DIRECT_LINE/UPDATE_CONNECTION_STATUS
10:19:32.057 chatStoreMiddleware.ts:66 PAYLOAD: {connectionStatus: 0}
10:19:32.057 chatStoreMiddleware.ts:64 >>> UPDATE_CONNECTION_STATUS
10:19:32.057 chatStoreMiddleware.ts:65 TYPE: DIRECT_LINE/UPDATE_CONNECTION_STATUS
10:19:32.057 chatStoreMiddleware.ts:66 PAYLOAD: {connectionStatus: 1}
10:19:32.058 chatStoreMiddleware.ts:64 >>> UPDATE_CONNECTION_STATUS
10:19:32.058 chatStoreMiddleware.ts:65 TYPE: DIRECT_LINE/UPDATE_CONNECTION_STATUS
10:19:32.058 chatStoreMiddleware.ts:66 PAYLOAD: {connectionStatus: 2}
10:19:32.069 chatStoreMiddleware.ts:104 >>> CONNECT_FULFILLED
10:19:32.069 chatStoreMiddleware.ts:105 TYPE: DIRECT_LINE/CONNECT_FULFILLED
10:19:32.069 chatStoreMiddleware.ts:106 PAYLOAD: {directLine: DirectLine}
Alright, we have all the events in order, so what's the problem? Let's add back the 'DIRECT_LINE/CONNECTION_STATUS_UPDATE' case and expose the freshly added DirectLineConnectionStatus.ExpiredToken status in 'DIRECT_LINE/UPDATE_CONNECTION_STATUS' case :
// code before omitted
switch (action.type) {
// initial case
case 'DIRECT_LINE/CONNECTION_STATUS_UPDATE': {
console.log('\n>>> CONNECTION_STATUS_UPDATE');
console.log('TYPE:', action.type);
console.log('PAYLOAD:', action.payload);
if (
action.payload &&
action.payload.connectionStatus ===
DirectLineConnectionStatus.Online
) {
handleConnectionOnline();
}
// unable to handle DirectLineConnectionStatus.ExpiredToken here
break;
}
// new case
case 'DIRECT_LINE/UPDATE_CONNECTION_STATUS': {
console.log('\n>>> UPDATE_CONNECTION_STATUS');
console.log('TYPE:', action.type);
console.log('PAYLOAD:', action.payload);
if (
action.payload &&
action.payload.connectionStatus ===
DirectLineConnectionStatus.ExpiredToken
) {
handleExpiredToken();
}
// do not handle DirectLineConnectionStatus.Online
break;
}
case 'DIRECT_LINE/CONNECT_FULFILLED': {
console.log('\n>>> CONNECT_FULFILLED');
console.log('TYPE:', action.type);
console.log('PAYLOAD:', action.payload);
// handle connect fulfilled
break;
}
// other cases omitted
}
// code after omitted
/**
* these get logged from the *new* case of 'DIRECT_LINE/UPDATE_CONNECTION_STATUS'
*/
13:00:43.375 chatStoreMiddleware.ts:64 >>> UPDATE_CONNECTION_STATUS
13:00:43.375 chatStoreMiddleware.ts:65 TYPE: DIRECT_LINE/UPDATE_CONNECTION_STATUS
13:00:43.375 chatStoreMiddleware.ts:66 PAYLOAD: {connectionStatus: 0}
13:00:43.376 chatStoreMiddleware.ts:64 >>> UPDATE_CONNECTION_STATUS
13:00:43.376 chatStoreMiddleware.ts:65 TYPE: DIRECT_LINE/UPDATE_CONNECTION_STATUS
13:00:43.376 chatStoreMiddleware.ts:66 PAYLOAD: {connectionStatus: 1}
13:00:43.376 chatStoreMiddleware.ts:64 >>> UPDATE_CONNECTION_STATUS
13:00:43.377 chatStoreMiddleware.ts:65 TYPE: DIRECT_LINE/UPDATE_CONNECTION_STATUS
13:00:43.377 chatStoreMiddleware.ts:66 PAYLOAD: {connectionStatus: 2}
/**
* CONNECT_FULFILLED acknowledgement
*/
13:00:43.379 chatStoreMiddleware.ts:104 >>> CONNECT_FULFILLED
13:00:43.379 chatStoreMiddleware.ts:105 TYPE: DIRECT_LINE/CONNECT_FULFILLED
13:00:43.379 chatStoreMiddleware.ts:106 PAYLOAD: {directLine: DirectLine}
/**
* these get logged from the *initial* case of 'DIRECT_LINE/CONNECTION_STATUS_UPDATE'
*/
13:00:43.381 chatStoreMiddleware.ts:91 >>> CONNECTION_STATUS_UPDATE
13:00:43.381 chatStoreMiddleware.ts:92 >>> TYPE: DIRECT_LINE/CONNECTION_STATUS_UPDATE
13:00:43.381 chatStoreMiddleware.ts:93 >>> PAYLOAD: {connectionStatus: 2}
Voila! the chat now starts!
So to the good part:
DIRECT_LINE/UPDATE_CONNECTION_STATUS can be used only for token expiration and 'DIRECT_LINE/CONNECTION_STATUS_UPDATE' only for checking that Direct Line is fully initialized and ready to accept messages?Not the first time seeing direct-line expired token issues. Some time ago created an issue with no resolution.
@cimbis,
It's true that documentation is lacking, however looking at the code for CONNECTION_STATUS_UPDATE, there is a note mentioning that this action should be obsolete, in favor of UPDATE_CONNECTION_STATUS. It goes on to say that both actions behave differently with CONNECTION_STATUS_UPDATE only dispatching after a connection is made and does not dispatch when disconnected. In short, you should refrain from using it.
It should also be noted that there is a bug around the "ExpiredToken" connection status that, if the bot is connecting to direct line via web socket, the connection status is not getting picked up.
That being said, in a nutshell, this is how I handle expiring / refreshing tokens. Note, I run everything locally but have implemented a version of this successfully in production, as a test. Most of what I describe I have parsed into separate files, processes, and functions for my own use. But, a basic implementation shouldn't be hard to achieve.
First, I run a "token" server that I use for making various HTTP calls to direct line and elsewhere for getting tokens, including refreshed tokens. From Web Chat, I make HTTP calls against my server to get these tokens.
In Web Chat, I save the original token in session storage. Then, in the web chat store I use setInterval(), set at 1500000 ms, which is shorter than the lifespan of the token before expiring. In setInterval() I call my server to get a refreshed token passing in the saved token from session storage. A successful response will return a new token, which I again save to session storage, and pass into createDirectLine() to maintain the connection.
I only call setInterval() when the store's action.type matches DIRECT_LINE/INCOMING_ACTIVITY, the activity.type is 'message', and the activity.from.role is 'users'. As every activity comes thru that matches that criteria, I clear any previous setInterval() and start a new one. In this way, if there is user activity the current token remains valid. If there is no user activity and the interval elapses, then the token is refreshed and the connection is maintained.
@cimbis Does the answer by @stevkan resolve your issue? It seems like it would.
Hi @stevkan, @cleemullins,
Thanks for your replies!
While the explanation provided by @stevkan might indeed be a good solution for keeping session alive, it does not really help with the case I mentioned.
Namely, once the user opens chat and then leaves it for x amount of time and, e.g., closes the lid of laptop - the auto-renewal won't work because there's no live connection for requests to get through.
I am trying to figure out appropriate solution for this type of situation, where session is bound to expire, so that my customers are able to continue with the chat flow without having to experience frozen application and restart the whole chat. How would that sort of implementation look like?
Also, when can community expect an upgrade to documentation, which would explain the usage of DIRECT_LINE connection statuses and session handling?
@cimbis, thank you for the clarification. I'm not aware of a method that would allow for this, but let me do a little research / testing and see what I may find.
@cimbis, It appears that the essence of what you are wanting can be achieved by use of the browser's built-in navigator.onLine API. When the browser goes online or offline, either by simulation (as shown below) or by sleeping/waking the machine (closing the laptop) or a loss of internet connectivity, an "online"/"offline" browser event is emitted setting the above API to a truthy value.
This value can be acted on via an event listener.
When the browser goes offline, we use the store to dispatch a DIRECT_LINE/DISCONNECT action which disconnects Web Chat and, effectively, disallows any new actions to be taken.
When the browser goes online, we again use the store to, this time, dispatch the DIRECT_LINE/RECONNECT action. Prior to the dispatch call, we first need to re-establish our connection to direct line. For this to happen, in short, you will want to call the reconnect API at
/v3/directline/conversations/{conversationId}/?watermark={watermark}
passing in the token, conversationId, and watermark (optional). The response will include a new token and streamUrl which, along with the conversationId (and watermark, if supplied), are necessary components when calling createDirectLine() to resume the conversation.
I tested this via simulating going offline as well as sleeping the laptop. In both instances, the conversation was able to be resumed. There were still errors that populated in the developer console, but they didn't hinder the re-connection nor affect the conversation.
Undoubtedly, this will require some massaging to capture different instances such as a closed web socket or an expired token if the machine is asleep longer than the token expiry.
Lastly, just of note, the DISCONNECT is actually dispatched when the machine is awoken which is the first instance the system is able to recognize it was not running. The RECONNECT occurs within seconds, afterwards.
Hope of help!
window.addEventListener('offline', (e) => {
store.dispatch({
type: 'DIRECT_LINE/DISCONNECT'
})
});
window.addEventListener('online', async (e) => {
<<RECONNECT TO DIRECT LINE>>
store.dispatch( {
type: 'DIRECT_LINE/RECONNECT'
} )
});

Thank you so much @stevkan! This looks like something I'm after.
I'll try out your proposed ideas next week - will let you know how it goes!
Closing the issue as resolved. Please feel free to reopen if the issue persists.