Skip to content

Commit d07e655

Browse files
compulimtdurnford
andauthored
Add networkInformation object to DLASE (#412)
* WIP: Add watchdog * Add warning if REST API returned too soon * Add integration test * Add NetworkInformation watchdog * Add network information watchdog * Prolong test time * Support NetworkInformation-like * Add AbortSignal function story * Add abort watchdog test * Add entry * Update comments * Add FAQ related to network change * Rename watchdog to (network) probe * Add NetworkInformationObserver * Update test names * Add WebSocketClient implementation * Add tests * Add test for change network type * Move to NetworkInformation * Fix flaky * Clean up * Explain NetworkInformation requirement * Allow unset networkInformation option * Clean up * Clean up * Remove NetworkInformation polyfill * Update SSE * Update entry * Add clarifications * Update to Node.js 18 * Add NetworkInformation.d.ts * Update README.md Co-authored-by: TJ Durnford <tjdford@gmail.com> * Typo --------- Co-authored-by: TJ Durnford <tjdford@gmail.com>
1 parent 21a5787 commit d07e655

19 files changed

Lines changed: 797 additions & 149 deletions

.github/workflows/continuous-deployment.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
node-version: [16.x]
15+
node-version: [18.x]
1616

1717
steps:
1818
- uses: actions/checkout@v3
@@ -48,7 +48,7 @@ jobs:
4848
steps:
4949
- uses: actions/setup-node@v3
5050
with:
51-
node-version: 16
51+
node-version: 18
5252
registry-url: https://registry.npmjs.org/
5353
- name: Download tarball artifact
5454
uses: actions/download-artifact@v3.0.1

.github/workflows/publish-release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ jobs:
1010

1111
steps:
1212
- uses: actions/checkout@v3
13-
- name: Use Node.js 16
13+
- name: Use Node.js 18
1414
uses: actions/setup-node@v3
1515
with:
16-
node-version: 16
16+
node-version: 18
1717
cache: 'npm'
1818
- id: get-version
1919
name: Get version
@@ -49,7 +49,7 @@ jobs:
4949
steps:
5050
- uses: actions/setup-node@v3
5151
with:
52-
node-version: 16
52+
node-version: 18
5353
registry-url: https://registry.npmjs.org/
5454
- name: Download tarball artifact
5555
uses: actions/download-artifact@v3.0.1

.github/workflows/pull-request-validation.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
node-version: [16.x]
15+
node-version: [18.x]
1616

1717
steps:
1818
- uses: actions/checkout@v3

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1616

1717
## [Unreleased]
1818

19+
### Added
20+
21+
- Direct Line Streaming: Added `networkInformation` option to assist detection of connection issues, by [@compulim](https://github.com/compulim), in PR [#412](https://github.com/microsoft/BotFramework-DirectLineJS/pull/412)
22+
1923
## [0.15.4] - 2023-06-05
2024

2125
### Changed

README.md

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66

77
[![Build Status](https://travis-ci.org/Microsoft/BotFramework-DirectLineJS.svg?branch=master)](https://travis-ci.org/Microsoft/BotFramework-DirectLineJS)
88

9-
Client library for the [Microsoft Bot Framework](http://www.botframework.com) *[Direct Line](https://docs.botframework.com/en-us/restapi/directline3/)* protocol.
9+
Client library for the [Microsoft Bot Framework](http://www.botframework.com) _[Direct Line](https://docs.botframework.com/en-us/restapi/directline3/)_ protocol.
1010

1111
Used by [WebChat](https://github.com/Microsoft/BotFramework-WebChat) and thus (by extension) [Emulator](https://github.com/Microsoft/BotFramework-Emulator), WebChat channel, and [Azure Bot Service](https://azure.microsoft.com/en-us/services/bot-service/).
1212

1313
## FAQ
1414

15-
### *Who is this for?*
15+
### _Who is this for?_
1616

1717
Anyone who is building a Bot Framework JavaScript client who does not want to use [WebChat](https://github.com/Microsoft/BotFramework-WebChat).
1818

1919
If you're currently using WebChat, you don't need to make any changes as it includes this package.
2020

21-
### *What is that funny `subscribe()` method in the samples below?*
21+
### _What is that funny `subscribe()` method in the samples below?_
2222

2323
Instead of callbacks or Promises, this library handles async operations using Observables. Try it, you'll like it! For more information, check out [RxJS](https://github.com/reactivex/rxjs/).
2424

25-
### *Can I use [TypeScript](http://www.typescriptlang.com)?*
25+
### _Can I use [TypeScript](http://www.typescriptlang.com)?_
2626

2727
You bet.
2828

@@ -32,6 +32,22 @@ This is an official Microsoft-supported library, and is considered largely compl
3232

3333
That said, the public API is still subject to change.
3434

35+
### Why the library did not detect Web Socket disconnections?
36+
37+
On iOS/iPadOS, when network change from Wi-Fi to cellular, the `WebSocket` object will be stalled without any errors. This is not detectable nor workaroundable without any additional assistance. The issue is related to an experimental feature named "NSURLSession WebSocket". The feature is enabled by default on iOS/iPadOS 15 and up.
38+
39+
An option named `networkInformation` can be used to assist the library to detect any connection issues. The option is based on [W3C Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) and it should implement at least 2 members:
40+
41+
- [A `type` property](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/type) to indicate the current network type
42+
- When the `type` is `"offline"`, network is not available and no connection will be made
43+
- [A `change` event](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/change_event) should dispatch when the `type` property change
44+
45+
However, Safari on iOS/iPadOS [does not support W3C Network Information API](https://bugs.webkit.org/show_bug.cgi?id=185697). It is up to web developers to implement the `NetworkInformation` polyfill.
46+
47+
One effective way to detect network type change is to subscribe to a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) source. The service would send a message every 30 seconds. If network type changed and current network type is no longer available, the connection will be closed prematurely and an `error` event will be dispatched to the [`EventSource`](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) instance. Upon receiving the `error` event, the `NetworkInformation.type` should then change to `"offline"`. The browser would automatically retry the Server-Sent Events connection. Upon receiving an `open` event, the polyfill should change the `type` back to `"unknown"`.
48+
49+
If the library is being used in a native iOS/iPadOS app, a less resource-intensive solution would be partially implementing the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) using [`NWPathMonitor`](https://developer.apple.com/documentation/network/nwpathmonitor). When network change happens, the `NetworkInformation` instance should update the [`type` property](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/type) based on network type and dispatch a [`change` event](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/change_event).
50+
3551
## How to build from source
3652

3753
0. Clone this repo
@@ -88,65 +104,58 @@ var directLine = new DirectLine({
88104
### Post activities to the bot:
89105

90106
```typescript
91-
directLine.postActivity({
107+
directLine
108+
.postActivity({
92109
from: { id: 'myUserId', name: 'myUserName' }, // required (from.name is optional)
93110
type: 'message',
94111
text: 'a message for you, Rudy'
95-
}).subscribe(
96-
id => console.log("Posted activity, assigned ID ", id),
97-
error => console.log("Error posting activity", error)
98-
);
112+
})
113+
.subscribe(
114+
id => console.log('Posted activity, assigned ID ', id),
115+
error => console.log('Error posting activity', error)
116+
);
99117
```
100118

101119
You can also post messages with attachments, and non-message activities such as events, by supplying the appropriate fields in the activity.
102120

103121
### Listen to activities sent from the bot:
104122

105123
```typescript
106-
directLine.activity$
107-
.subscribe(
108-
activity => console.log("received activity ", activity)
109-
);
124+
directLine.activity$.subscribe(activity => console.log('received activity ', activity));
110125
```
111126

112127
You can use RxJS operators on incoming activities. To see only message activities:
113128

114129
```typescript
115130
directLine.activity$
116-
.filter(activity => activity.type === 'message')
117-
.subscribe(
118-
message => console.log("received message ", message)
119-
);
131+
.filter(activity => activity.type === 'message')
132+
.subscribe(message => console.log('received message ', message));
120133
```
121134

122135
Direct Line will helpfully send your client a copy of every sent activity, so a common pattern is to filter incoming messages on `from`:
123136

124137
```typescript
125138
directLine.activity$
126-
.filter(activity => activity.type === 'message' && activity.from.id === 'yourBotHandle')
127-
.subscribe(
128-
message => console.log("received message ", message)
129-
);
139+
.filter(activity => activity.type === 'message' && activity.from.id === 'yourBotHandle')
140+
.subscribe(message => console.log('received message ', message));
130141
```
131142

132143
### Monitor connection status
133144

134145
Subscribing to either `postActivity` or `activity$` will start the process of connecting to the bot. Your app can listen to the connection status and react appropriately :
135146

136147
```typescript
137-
138148
import { ConnectionStatus } from 'botframework-directlinejs';
139149

140-
directLine.connectionStatus$
141-
.subscribe(connectionStatus => {
142-
switch(connectionStatus) {
143-
case ConnectionStatus.Uninitialized: // the status when the DirectLine object is first created/constructed
144-
case ConnectionStatus.Connecting: // currently trying to connect to the conversation
145-
case ConnectionStatus.Online: // successfully connected to the converstaion. Connection is healthy so far as we know.
146-
case ConnectionStatus.ExpiredToken: // last operation errored out with an expired token. Your app should supply a new one.
147-
case ConnectionStatus.FailedToConnect: // the initial attempt to connect to the conversation failed. No recovery possible.
148-
case ConnectionStatus.Ended: // the bot ended the conversation
149-
}
150+
directLine.connectionStatus$.subscribe(connectionStatus => {
151+
switch (connectionStatus) {
152+
case ConnectionStatus.Uninitialized: // the status when the DirectLine object is first created/constructed
153+
case ConnectionStatus.Connecting: // currently trying to connect to the conversation
154+
case ConnectionStatus.Online: // successfully connected to the converstaion. Connection is healthy so far as we know.
155+
case ConnectionStatus.ExpiredToken: // last operation errored out with an expired token. Your app should supply a new one.
156+
case ConnectionStatus.FailedToConnect: // the initial attempt to connect to the conversation failed. No recovery possible.
157+
case ConnectionStatus.Ended: // the bot ended the conversation
158+
}
150159
});
151160
```
152161

@@ -168,7 +177,8 @@ directLine.reconnect(conversation);
168177
When using DirectLine with WebChat, closing the current tab or refreshing the page will create a new conversation in most cases. You can resume an existing conversation to keep the user in the same context.
169178

170179
**When using a secret** you can resume a conversation by:
171-
- Storing the conversationid (in a *permanent* place, like local storage)
180+
181+
- Storing the conversationid (in a _permanent_ place, like local storage)
172182
- Giving this value back while creating the DirectLine object along with the secret
173183

174184
```typescript
@@ -181,7 +191,8 @@ const dl = new DirectLine({
181191
```
182192

183193
**When using a token** you can resume a conversation by:
184-
- Storing the conversationid and your token (in a *permanent* place, like local storage)
194+
195+
- Storing the conversationid and your token (in a _permanent_ place, like local storage)
185196
- Calling the DirectLine reconnect API yourself to get a refreshed token and a streamurl
186197
- Creating the DirectLine object using the ConversationId, Token, and StreamUrl
187198

@@ -196,7 +207,7 @@ const dl = new DirectLine({
196207
```
197208

198209
**Getting any history that Direct Line has cached** : you can retrieve history using watermarks:
199-
You can see the watermark as an *activity 'bookmark'*. The resuming scenario will replay all the conversation activities from the watermark you specify.
210+
You can see the watermark as an _activity 'bookmark'_. The resuming scenario will replay all the conversation activities from the watermark you specify.
200211

201212
```typescript
202213
import { DirectLine } from 'botframework-directlinejs';
@@ -212,7 +223,7 @@ const dl = new DirectLine({
212223

213224
## Contributing
214225

215-
This project welcomes contributions and suggestions. Most contributions require you to agree to a
226+
This project welcomes contributions and suggestions. Most contributions require you to agree to a
216227
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
217228
the rights to use your contribution. For details, visit https://cla.microsoft.com.
218229

@@ -228,6 +239,7 @@ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.
228239
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
229240

230241
## Reporting Security Issues
242+
231243
Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) at [secure@microsoft.com](mailto:secure@microsoft.com). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the [MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in the [Security TechCenter](https://technet.microsoft.com/en-us/security/default).
232244

233245
Copyright (c) Microsoft Corporation. All rights reserved.

__tests__/directLineStreaming/__setup__/createBotProxy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export default function createBotProxy(init?: CreateBotProxyInit): Promise<Creat
143143
webSocketProxy.emit('connection', ws, proxySocket, req)
144144
)
145145
);
146+
proxySocket.addEventListener('error', () => {});
146147

147148
socket.once('close', () => proxySocket.close());
148149
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import '@jest/types';
2+
3+
declare global {
4+
namespace jest {
5+
interface Expect {
6+
activityContaining(messageText: string, mergeActivity?: { id?: string; type?: string }): any;
7+
}
8+
}
9+
}

__tests__/directLineStreaming/__setup__/mockObserver.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,24 @@ type Observation<T> =
1212
* Mocks an observer and records all observations.
1313
*/
1414
export default function mockObserver<T>(): Readonly<
15-
Required<Observer<T>> & { observations: ReadonlyArray<Observation<T>> }
15+
Required<Observer<T>> & {
16+
observations: ReadonlyArray<Observation<T>>;
17+
observe: (observation: Observation<T>) => void;
18+
}
1619
> {
20+
const observe: (observation: Observation<T>) => void = jest.fn(observation => observations.push(observation));
1721
const observations: Array<Observation<T>> = [];
1822

19-
const complete = jest.fn(() => observations.push([Date.now(), 'complete']));
20-
const error = jest.fn(reason => observations.push([Date.now(), 'error', reason]));
21-
const next = jest.fn(value => observations.push([Date.now(), 'next', value]));
22-
const start = jest.fn(subscription => observations.push([Date.now(), 'start', subscription]));
23+
const complete = jest.fn(() => observe([Date.now(), 'complete']));
24+
const error = jest.fn(reason => observe([Date.now(), 'error', reason]));
25+
const next = jest.fn(value => observe([Date.now(), 'next', value]));
26+
const start = jest.fn(subscription => observe([Date.now(), 'start', subscription]));
2327

2428
return Object.freeze({
2529
complete,
2630
error,
2731
next,
32+
observe,
2833
start,
2934

3035
get observations() {

__tests__/directLineStreaming/connect.fail.story.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ test('connect fail should signal properly', async () => {
4343
directLine.activity$.subscribe(activityObserver);
4444

4545
// THEN: Should try to connect 3 times.
46-
await waitFor(() => expect(onUpgrade).toBeCalledTimes(3));
46+
await waitFor(() => expect(onUpgrade).toBeCalledTimes(3), { timeout: 5_000 });
4747

4848
// THEN: Should not wait before connecting the first time.
4949
expect(onUpgrade.mock.results[0].value - connectTime).toBeLessThan(3000);

0 commit comments

Comments
 (0)