Skip to content

Commit 5bf8b16

Browse files
committed
feat: Add connection mananger. (#522)
This adds a connection manager which is used to handle application state and network state and determine the desired connection mode.
1 parent 7886daf commit 5bf8b16

File tree

3 files changed

+363
-1
lines changed

3 files changed

+363
-1
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { BasicLogger, ConnectionMode, LDLogger } from '@launchdarkly/js-client-sdk-common';
2+
3+
import {
4+
ApplicationState,
5+
ConnectionDestination,
6+
ConnectionManager,
7+
NetworkState,
8+
StateDetector,
9+
} from './ConnectionManager';
10+
11+
function mockDestination(): ConnectionDestination {
12+
return {
13+
setNetworkAvailability: jest.fn(),
14+
setEventSendingEnabled: jest.fn(),
15+
setConnectionMode: jest.fn(),
16+
flush: jest.fn(),
17+
};
18+
}
19+
20+
class MockDetector implements StateDetector {
21+
appStateListener?: (state: ApplicationState) => void;
22+
networkStateListener?: (state: NetworkState) => void;
23+
24+
setApplicationStateListener(fn: (state: ApplicationState) => void): void {
25+
this.appStateListener = fn;
26+
}
27+
setNetworkStateListener(fn: (state: NetworkState) => void): void {
28+
this.networkStateListener = fn;
29+
}
30+
stopListening(): void {
31+
this.appStateListener = undefined;
32+
this.networkStateListener = undefined;
33+
}
34+
}
35+
36+
describe.each<ConnectionMode>(['streaming', 'polling'])(
37+
'given initial connection modes',
38+
(initialConnectionMode) => {
39+
let destination: ConnectionDestination;
40+
let stateDetector: MockDetector;
41+
let logDestination: jest.Mock;
42+
let logger: LDLogger;
43+
44+
beforeEach(() => {
45+
destination = mockDestination();
46+
stateDetector = new MockDetector();
47+
logDestination = jest.fn();
48+
logger = new BasicLogger({ destination: logDestination });
49+
});
50+
51+
it('can set the connection offline when entering the background', () => {
52+
// eslint-disable-next-line no-new
53+
new ConnectionManager(
54+
logger,
55+
{
56+
initialConnectionMode,
57+
runInBackground: false,
58+
automaticBackgroundHandling: true,
59+
automaticNetworkHandling: true,
60+
},
61+
destination,
62+
stateDetector,
63+
);
64+
stateDetector.appStateListener!(ApplicationState.Background);
65+
66+
expect(destination.setConnectionMode).toHaveBeenCalledWith('offline');
67+
});
68+
69+
it('can restore the connection when entering the foreground mode', () => {
70+
// eslint-disable-next-line no-new
71+
new ConnectionManager(
72+
logger,
73+
{
74+
initialConnectionMode,
75+
runInBackground: false,
76+
automaticBackgroundHandling: true,
77+
automaticNetworkHandling: true,
78+
},
79+
destination,
80+
stateDetector,
81+
);
82+
stateDetector.appStateListener!(ApplicationState.Background);
83+
stateDetector.appStateListener!(ApplicationState.Foreground);
84+
85+
expect(destination.setConnectionMode).toHaveBeenNthCalledWith(1, 'offline');
86+
expect(destination.setConnectionMode).toHaveBeenNthCalledWith(2, initialConnectionMode);
87+
expect(destination.setConnectionMode).toHaveBeenCalledTimes(2);
88+
});
89+
90+
it('can continue to run in the background when configured to do so', () => {
91+
// eslint-disable-next-line no-new
92+
new ConnectionManager(
93+
logger,
94+
{
95+
initialConnectionMode,
96+
runInBackground: true,
97+
automaticBackgroundHandling: true,
98+
automaticNetworkHandling: true,
99+
},
100+
destination,
101+
stateDetector,
102+
);
103+
stateDetector.appStateListener!(ApplicationState.Background);
104+
stateDetector.appStateListener!(ApplicationState.Foreground);
105+
expect(destination.setConnectionMode).toHaveBeenNthCalledWith(1, initialConnectionMode);
106+
expect(destination.setConnectionMode).toHaveBeenNthCalledWith(2, initialConnectionMode);
107+
expect(destination.setConnectionMode).toHaveBeenCalledTimes(2);
108+
});
109+
110+
it('set the network availability to false when it detects the network is not available', () => {
111+
// eslint-disable-next-line no-new
112+
new ConnectionManager(
113+
logger,
114+
{
115+
initialConnectionMode,
116+
runInBackground: true,
117+
automaticBackgroundHandling: true,
118+
automaticNetworkHandling: true,
119+
},
120+
destination,
121+
stateDetector,
122+
);
123+
stateDetector.networkStateListener!(NetworkState.Unavailable);
124+
expect(destination.setNetworkAvailability).toHaveBeenCalledWith(false);
125+
expect(destination.setNetworkAvailability).toHaveBeenCalledTimes(1);
126+
});
127+
128+
it('sets the network availability to true when it detects the network is available', () => {
129+
// eslint-disable-next-line no-new
130+
new ConnectionManager(
131+
logger,
132+
{
133+
initialConnectionMode,
134+
runInBackground: true,
135+
automaticBackgroundHandling: true,
136+
automaticNetworkHandling: true,
137+
},
138+
destination,
139+
stateDetector,
140+
);
141+
stateDetector.networkStateListener!(NetworkState.Unavailable);
142+
stateDetector.networkStateListener!(NetworkState.Available);
143+
expect(destination.setNetworkAvailability).toHaveBeenNthCalledWith(1, false);
144+
expect(destination.setNetworkAvailability).toHaveBeenNthCalledWith(2, true);
145+
expect(destination.setNetworkAvailability).toHaveBeenCalledTimes(2);
146+
});
147+
148+
it('remains offline when temporarily offline', () => {
149+
// eslint-disable-next-line no-new
150+
const connectionManager = new ConnectionManager(
151+
logger,
152+
{
153+
initialConnectionMode,
154+
runInBackground: true,
155+
automaticBackgroundHandling: true,
156+
automaticNetworkHandling: true,
157+
},
158+
destination,
159+
stateDetector,
160+
);
161+
connectionManager.setOffline(true);
162+
163+
stateDetector.appStateListener!(ApplicationState.Background);
164+
stateDetector.appStateListener!(ApplicationState.Foreground);
165+
166+
expect(destination.setConnectionMode).toHaveBeenNthCalledWith(1, 'offline');
167+
expect(destination.setConnectionMode).toHaveBeenNthCalledWith(2, 'offline');
168+
expect(destination.setConnectionMode).toHaveBeenNthCalledWith(3, 'offline');
169+
expect(destination.setConnectionMode).toHaveBeenCalledTimes(3);
170+
});
171+
172+
it('ignores application state changes when automaticBackgroundHandling is disabled', () => {
173+
// eslint-disable-next-line no-new
174+
new ConnectionManager(
175+
logger,
176+
{
177+
initialConnectionMode,
178+
runInBackground: true,
179+
automaticBackgroundHandling: false,
180+
automaticNetworkHandling: true,
181+
},
182+
destination,
183+
stateDetector,
184+
);
185+
stateDetector.appStateListener?.(ApplicationState.Background);
186+
stateDetector.appStateListener?.(ApplicationState.Foreground);
187+
188+
expect(destination.setConnectionMode).toHaveBeenCalledTimes(0);
189+
});
190+
191+
it('ignores network state changes when automaticNetworkHandling is disabled', () => {
192+
// eslint-disable-next-line no-new
193+
new ConnectionManager(
194+
logger,
195+
{
196+
initialConnectionMode,
197+
runInBackground: true,
198+
automaticBackgroundHandling: true,
199+
automaticNetworkHandling: false,
200+
},
201+
destination,
202+
stateDetector,
203+
);
204+
stateDetector.networkStateListener?.(NetworkState.Unavailable);
205+
stateDetector.networkStateListener?.(NetworkState.Available);
206+
expect(destination.setNetworkAvailability).toHaveBeenCalledTimes(0);
207+
});
208+
},
209+
);
210+
211+
describe.each(['offline', 'streaming', 'polling'])('given requested connection modes', () => {
212+
it('respects changes to the desired connection mode', () => {});
213+
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { ConnectionMode, LDLogger } from '@launchdarkly/js-client-sdk-common';
2+
3+
export enum ApplicationState {
4+
/// The application is in the foreground.
5+
Foreground = 'foreground',
6+
7+
/// The application is in the background.
8+
///
9+
/// Note, the application will not be active while in the background, but
10+
/// it will track when it is entering or exiting a background state.
11+
Background = 'background',
12+
}
13+
14+
export enum NetworkState {
15+
/// There is no network available for the SDK to use.
16+
Unavailable = 'unavailable',
17+
18+
/// The network is available. Note that network requests may still fail
19+
/// for other reasons.
20+
Available = 'available',
21+
}
22+
23+
export interface ConnectionDestination {
24+
setNetworkAvailability(available: boolean): void;
25+
setEventSendingEnabled(enabled: boolean, flush: boolean): void;
26+
setConnectionMode(mode: ConnectionMode): Promise<void>;
27+
flush(): Promise<void>;
28+
}
29+
30+
export interface StateDetector {
31+
setApplicationStateListener(fn: (state: ApplicationState) => void): void;
32+
setNetworkStateListener(fn: (state: NetworkState) => void): void;
33+
34+
stopListening(): void;
35+
}
36+
37+
export interface ConnectionManagerConfig {
38+
/// The initial connection mode the SDK should use.
39+
readonly initialConnectionMode: ConnectionMode;
40+
41+
/// Some platforms (windows, web, mac, linux) can continue executing code
42+
/// in the background.
43+
readonly runInBackground: boolean;
44+
45+
/// Enable handling of network availability. When this is true the
46+
/// connection state will automatically change when network
47+
/// availability changes.
48+
readonly automaticNetworkHandling: boolean;
49+
50+
/// Enable handling associated with transitioning between the foreground
51+
/// and background.
52+
readonly automaticBackgroundHandling: boolean;
53+
}
54+
55+
export class ConnectionManager {
56+
private applicationState: ApplicationState = ApplicationState.Foreground;
57+
private networkState: NetworkState = NetworkState.Available;
58+
private offline: boolean = false;
59+
private currentConnectionMode: ConnectionMode;
60+
61+
constructor(
62+
private readonly logger: LDLogger,
63+
private readonly config: ConnectionManagerConfig,
64+
private readonly destination: ConnectionDestination,
65+
private readonly detector: StateDetector,
66+
) {
67+
this.currentConnectionMode = config.initialConnectionMode;
68+
if (config.automaticBackgroundHandling) {
69+
detector.setApplicationStateListener((state) => {
70+
this.applicationState = state;
71+
this.handleState();
72+
});
73+
}
74+
if (config.automaticNetworkHandling) {
75+
detector.setNetworkStateListener((state) => {
76+
this.networkState = state;
77+
this.handleState();
78+
});
79+
}
80+
}
81+
82+
public setOffline(offline: boolean): void {
83+
this.offline = offline;
84+
this.handleState();
85+
}
86+
87+
public setConnectionMode(mode: ConnectionMode) {
88+
this.currentConnectionMode = mode;
89+
this.handleState();
90+
}
91+
92+
public close() {
93+
this.detector.stopListening();
94+
}
95+
96+
private handleState(): void {
97+
this.logger.debug(`Handling state: ${this.applicationState}:${this.networkState}`);
98+
99+
switch (this.networkState) {
100+
case NetworkState.Unavailable:
101+
this.destination.setNetworkAvailability(false);
102+
break;
103+
case NetworkState.Available:
104+
this.destination.setNetworkAvailability(true);
105+
switch (this.applicationState) {
106+
case ApplicationState.Foreground:
107+
this.setForegroundAvailable();
108+
break;
109+
case ApplicationState.Background:
110+
this.setBackgroundAvailable();
111+
break;
112+
default:
113+
break;
114+
}
115+
break;
116+
default:
117+
break;
118+
}
119+
}
120+
121+
private setForegroundAvailable(): void {
122+
if (this.offline) {
123+
this.destination.setConnectionMode('offline');
124+
this.destination.setEventSendingEnabled(false, false);
125+
return;
126+
}
127+
128+
// Currently the foreground mode will always be whatever the last active
129+
// connection mode was.
130+
this.destination.setConnectionMode(this.currentConnectionMode);
131+
this.destination.setEventSendingEnabled(true, false);
132+
}
133+
134+
private setBackgroundAvailable(): void {
135+
this.destination.flush();
136+
137+
if (!this.config.runInBackground) {
138+
this.destination.setConnectionMode('offline');
139+
this.destination.setEventSendingEnabled(false, false);
140+
return;
141+
}
142+
143+
// This SDK doesn't currently support automatic background polling.
144+
145+
// If connections in the background are allowed, then use the same mode
146+
// as is configured for the foreground.
147+
this.setForegroundAvailable();
148+
}
149+
}

packages/shared/sdk-client/src/api/ConnectionMode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
*
1010
* streaming - The SDK will use a streaming connection to receive updates from LaunchDarkly.
1111
*/
12-
type ConnectionMode = 'offline' | 'streaming';
12+
type ConnectionMode = 'offline' | 'streaming' | 'polling';
1313

1414
export default ConnectionMode;

0 commit comments

Comments
 (0)