Skip to content

Commit f5b81e6

Browse files
committed
feat: Refactor application state handling. (#523)
1 parent 5bf8b16 commit f5b81e6

File tree

10 files changed

+145
-190
lines changed

10 files changed

+145
-190
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { AppState, AppStateStatus } from 'react-native';
2+
3+
import { ApplicationState, NetworkState, StateDetector } from './platform/ConnectionManager';
4+
5+
function translateAppState(state: AppStateStatus): ApplicationState {
6+
switch (state) {
7+
case 'active':
8+
return ApplicationState.Foreground;
9+
case 'inactive':
10+
case 'background':
11+
case 'extension':
12+
default:
13+
return ApplicationState.Background;
14+
}
15+
}
16+
17+
export default class RNStateDetector implements StateDetector {
18+
private applicationStateListener?: (state: ApplicationState) => void;
19+
private networkStateListener?: (state: NetworkState) => void;
20+
21+
constructor() {
22+
AppState.addEventListener('change', (state: AppStateStatus) => {
23+
this.applicationStateListener?.(translateAppState(state));
24+
});
25+
}
26+
27+
setApplicationStateListener(fn: (state: ApplicationState) => void): void {
28+
this.applicationStateListener = fn;
29+
// When you listen provide the current state immediately.
30+
this.applicationStateListener(translateAppState(AppState.currentState));
31+
}
32+
33+
setNetworkStateListener(fn: (state: NetworkState) => void): void {
34+
this.networkStateListener = fn;
35+
// Not implemented.
36+
}
37+
38+
stopListening(): void {
39+
this.applicationStateListener = undefined;
40+
this.networkStateListener = undefined;
41+
}
42+
}

packages/sdk/react-native/src/ReactNativeLDClient.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
/* eslint-disable max-classes-per-file */
12
import {
23
AutoEnvAttributes,
34
base64UrlEncode,
45
BasicLogger,
6+
ConnectionMode,
57
internal,
68
LDClientImpl,
79
type LDContext,
810
type LDOptions,
911
} from '@launchdarkly/js-client-sdk-common';
1012

1113
import createPlatform from './platform';
14+
import { ConnectionDestination, ConnectionManager } from './platform/ConnectionManager';
15+
import RNStateDetector from './RNStateDetector';
1216

1317
/**
1418
* The React Native LaunchDarkly client. Instantiate this class to create an
@@ -24,6 +28,7 @@ import createPlatform from './platform';
2428
* ```
2529
*/
2630
export default class ReactNativeLDClient extends LDClientImpl {
31+
private connectionManager: ConnectionManager;
2732
/**
2833
* Creates an instance of the LaunchDarkly client.
2934
*
@@ -57,9 +62,51 @@ export default class ReactNativeLDClient extends LDClientImpl {
5762
{ ...options, logger },
5863
internalOptions,
5964
);
65+
66+
const destination: ConnectionDestination = {
67+
setNetworkAvailability: (available: boolean) => {
68+
this.setNetworkAvailability(available);
69+
},
70+
setEventSendingEnabled: (enabled: boolean, flush: boolean) => {
71+
this.setEventSendingEnabled(enabled, flush);
72+
},
73+
setConnectionMode: async (mode: ConnectionMode) => {
74+
// Pass the connection mode to the base implementation.
75+
// The RN implementation will pass the connection mode through the connection manager.
76+
this.baseSetConnectionMode(mode);
77+
},
78+
};
79+
80+
const initialConnectionMode = options.initialConnectionMode ?? 'streaming';
81+
this.connectionManager = new ConnectionManager(
82+
logger,
83+
{
84+
initialConnectionMode,
85+
// TODO: Add the ability to configure connection management.
86+
// This is not yet added as the RN SDK needs package specific configuration added.
87+
automaticNetworkHandling: true,
88+
automaticBackgroundHandling: true,
89+
runInBackground: false,
90+
},
91+
destination,
92+
new RNStateDetector(),
93+
);
94+
}
95+
96+
private baseSetConnectionMode(mode: ConnectionMode) {
97+
// Jest had problems with calls to super from nested arrow functions, so this method proxies the call.
98+
super.setConnectionMode(mode);
6099
}
61100

62101
override createStreamUriPath(context: LDContext) {
63102
return `/meval/${base64UrlEncode(JSON.stringify(context), this.platform.encoding!)}`;
64103
}
104+
105+
override async setConnectionMode(mode: ConnectionMode): Promise<void> {
106+
// Set the connection mode before setting offline, in case there is any mode transition work
107+
// such as flushing on entering the background.
108+
this.connectionManager.setConnectionMode(mode);
109+
// For now the data source connection and the event processing state are connected.
110+
this.connectionManager.setOffline(mode === 'offline');
111+
}
65112
}

packages/sdk/react-native/src/platform/ConnectionManager.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ function mockDestination(): ConnectionDestination {
1313
setNetworkAvailability: jest.fn(),
1414
setEventSendingEnabled: jest.fn(),
1515
setConnectionMode: jest.fn(),
16-
flush: jest.fn(),
1716
};
1817
}
1918

packages/sdk/react-native/src/platform/ConnectionManager.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ export interface ConnectionDestination {
2424
setNetworkAvailability(available: boolean): void;
2525
setEventSendingEnabled(enabled: boolean, flush: boolean): void;
2626
setConnectionMode(mode: ConnectionMode): Promise<void>;
27-
flush(): Promise<void>;
2827
}
2928

3029
export interface StateDetector {
@@ -121,6 +120,8 @@ export class ConnectionManager {
121120
private setForegroundAvailable(): void {
122121
if (this.offline) {
123122
this.destination.setConnectionMode('offline');
123+
// Don't attempt to flush. If the user wants to flush when entering offline
124+
// mode, then they can do that directly.
124125
this.destination.setEventSendingEnabled(false, false);
125126
return;
126127
}
@@ -132,11 +133,9 @@ export class ConnectionManager {
132133
}
133134

134135
private setBackgroundAvailable(): void {
135-
this.destination.flush();
136-
137136
if (!this.config.runInBackground) {
138137
this.destination.setConnectionMode('offline');
139-
this.destination.setEventSendingEnabled(false, false);
138+
this.destination.setEventSendingEnabled(false, true);
140139
return;
141140
}
142141

packages/sdk/react-native/src/platform/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,14 @@ import AsyncStorage from './ConditionalAsyncStorage';
2323
import PlatformCrypto from './crypto';
2424

2525
export class PlatformRequests implements Requests {
26-
eventSource?: RNEventSource<EventName>;
27-
2826
constructor(private readonly logger: LDLogger) {}
2927

3028
createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource {
31-
this.eventSource = new RNEventSource<EventName>(url, {
29+
return new RNEventSource<EventName>(url, {
3230
headers: eventSourceInitDict.headers,
3331
retryAndHandleError: eventSourceInitDict.errorFilter,
3432
logger: this.logger,
3533
});
36-
37-
return this.eventSource;
3834
}
3935

4036
fetch(url: string, options?: Options): Promise<Response> {

packages/sdk/react-native/src/provider/LDProvider.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import ReactNativeLDClient from '../ReactNativeLDClient';
77
import LDProvider from './LDProvider';
88
import setupListeners from './setupListeners';
99

10-
jest.mock('../provider/useAppState');
1110
jest.mock('../ReactNativeLDClient');
1211
jest.mock('./setupListeners');
1312

packages/sdk/react-native/src/provider/LDProvider.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import React, { PropsWithChildren, useEffect, useState } from 'react';
33
import ReactNativeLDClient from '../ReactNativeLDClient';
44
import { Provider, ReactContext } from './reactContext';
55
import setupListeners from './setupListeners';
6-
import useAppState from './useAppState';
76

87
type LDProps = {
98
client: ReactNativeLDClient;
@@ -26,8 +25,6 @@ const LDProvider = ({ client, children }: PropsWithChildren<LDProps>) => {
2625
setupListeners(client, setState);
2726
}, []);
2827

29-
useAppState(client);
30-
3128
return <Provider value={state}>{children}</Provider>;
3229
};
3330

packages/sdk/react-native/src/provider/useAppState.test.ts

Lines changed: 0 additions & 114 deletions
This file was deleted.

packages/sdk/react-native/src/provider/useAppState.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)