Skip to content

Commit d3db0d0

Browse files
authored
feat: enable dynamic API endpoint configuration (#151)
* refactor: rename variable * chore: remove jsdoc type * feat: allow dynamic api reconfiguration * fix: persist host configuration in storage * style: lint * fix: do not connect to devserver when running in prod mode * fix: do not connect to devserver twice devserver connection is initiated by start/startFromCache and should not happen after a request, unless the socket host changes handling this case will be moved to the socket class * fix: reconnect devserver on endpoint changes
1 parent 340d181 commit d3db0d0

File tree

10 files changed

+286
-35
lines changed

10 files changed

+286
-35
lines changed

src/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export default {
105105
VARIANTS: '__leanplum_variants',
106106
VARIANT_DEBUG_INFO: '__leanplum_variant_debug_info',
107107
ACTION_DEFINITIONS: '__leanplum_action_definitions',
108+
HOST_CONFIG: '__leanplum_hosts',
108109
INBOX_MESSAGES: '__leanplum_inbox_messages',
109110
TOKEN: '__leanplum_token',
110111
DEVICE_ID: '__leanplum_device_id',

src/LeanplumInternal.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export default class LeanplumInternal {
5151
this.createRequest.bind(this),
5252
this.onInboxAction.bind(this)
5353
)
54-
private _lpRequest: LeanplumRequest = new LeanplumRequest()
54+
private _lpRequest: LeanplumRequest = new LeanplumRequest(this._events)
5555
private _varCache: VarCache = new VarCache(this.createRequest.bind(this))
5656
private _lpSocket: LeanplumSocket = new LeanplumSocket(
5757
this._varCache,
@@ -88,6 +88,8 @@ export default class LeanplumInternal {
8888
}
8989
})
9090
this._events.on('registerForPush', () => this.registerForWebPush())
91+
this._events.on('updateDevServerHost',
92+
(host: string) => this.setSocketHost(host))
9193
}
9294

9395
setApiPath(apiPath: string): void {

src/LeanplumRequest.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ import ArgsBuilder from './ArgsBuilder'
1919
import Constants from './Constants'
2020
import StorageManager from './StorageManager'
2121
import Network from './Network'
22+
import EventEmitter from './EventEmitter'
23+
24+
interface HostConfig {
25+
apiHost: string;
26+
apiPath: string;
27+
devServerHost: string;
28+
}
2229

2330
export default class LeanplumRequest {
2431
private cooldownTimeout = null
@@ -34,8 +41,11 @@ export default class LeanplumRequest {
3441
public versionName: string
3542

3643
constructor(
44+
private events: EventEmitter,
3745
private network = new Network()
38-
) { }
46+
) {
47+
this.loadHostConfig()
48+
}
3949

4050
public get userId(): string | undefined {
4151
return this.userIdValue ?? this.loadLocal<string>(Constants.DEFAULT_KEYS.USER_ID) ?? this.deviceId
@@ -90,9 +100,8 @@ export default class LeanplumRequest {
90100
}
91101

92102
if (params.body()) {
93-
this.network.ajax(
94-
'POST',
95-
`${this.apiPath}?${argsBuilder.build()}`,
103+
this.sendRequest(
104+
`?${argsBuilder.build()}`,
96105
params.body(),
97106
success,
98107
error,
@@ -115,9 +124,8 @@ export default class LeanplumRequest {
115124
.add(Constants.PARAMS.ACTION, Constants.METHODS.MULTI)
116125
.add(Constants.PARAMS.TIME, (new Date().getTime() / 1000).toString().toString())
117126
.build()
118-
this.network.ajax(
119-
'POST',
120-
`${this.apiPath}?${multiRequestArgs}`,
127+
this.sendRequest(
128+
`?${multiRequestArgs}`,
121129
requestData,
122130
success,
123131
error,
@@ -179,6 +187,12 @@ export default class LeanplumRequest {
179187
return (count > 0) ? response?.response?.[count - 1] : null
180188
}
181189

190+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
191+
public getFirstResponse(response): any {
192+
const count = response?.response?.length ?? 0
193+
return (count > 0) ? response?.response?.[0] : null
194+
}
195+
182196
public isResponseSuccess(response): boolean {
183197
return Boolean(response?.success)
184198
}
@@ -191,6 +205,34 @@ export default class LeanplumRequest {
191205
this.saveLocal(Constants.DEFAULT_KEYS.COUNT, count)
192206
}
193207

208+
private sendRequest(query: string, data: string, success: Function, error: Function, queued: boolean): void {
209+
this.network.ajax(
210+
'POST',
211+
`${this.apiPath}${query}`,
212+
data,
213+
(response) => {
214+
const methodResponse = this.getFirstResponse(response)
215+
if (!methodResponse.success && methodResponse.apiHost) {
216+
const { apiHost, apiPath, devServerHost } = methodResponse
217+
218+
this.saveLocal(Constants.DEFAULT_KEYS.HOST_CONFIG, JSON.stringify({
219+
apiHost,
220+
apiPath,
221+
devServerHost,
222+
}))
223+
this.apiPath = `https://${apiHost}/${apiPath}`
224+
this.sendRequest(query, data, success, error, queued)
225+
226+
this.events.emit('updateDevServerHost', devServerHost)
227+
} else if (success) {
228+
success(response)
229+
}
230+
},
231+
error,
232+
queued
233+
)
234+
}
235+
194236
// eslint-disable-next-line @typescript-eslint/no-explicit-any
195237
private popUnsentRequests(): any[] {
196238
const requestData = []
@@ -213,6 +255,15 @@ export default class LeanplumRequest {
213255
return requestData
214256
}
215257

258+
private loadHostConfig(): void {
259+
const hostConfig = JSON.parse(this.loadLocal<string>(Constants.DEFAULT_KEYS.HOST_CONFIG) || 'null')
260+
if (hostConfig) {
261+
const { apiHost, apiPath, devServerHost } = hostConfig
262+
this.apiPath = `https://${apiHost}/${apiPath}`
263+
this.events.emit('updateDevServerHost', devServerHost)
264+
}
265+
}
266+
216267
private loadLocal<T>(key: string): T {
217268
return StorageManager.get(key)
218269
}

src/LeanplumSocket.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,16 @@ import EventEmitter from './EventEmitter'
2727
type RegisterMessage = { email: string }
2828
type DevServerMessage = RegisterMessage | Message
2929

30+
type AuthInfo = {
31+
appId: string;
32+
deviceId: string;
33+
}
34+
3035
export default class LeanplumSocket {
3136
private networkTimeoutSeconds = 10
3237
private socketClient: SocketIoClient | null = null
3338
private socketHost = 'dev.leanplum.com'
39+
private auth: AuthInfo = null
3440

3541
constructor(
3642
private cache: VarCache,
@@ -40,14 +46,14 @@ export default class LeanplumSocket {
4046
private events: EventEmitter,
4147
) { }
4248

43-
public connect(
44-
auth: { appId: string; deviceId: string }
45-
): void {
49+
public connect(auth: AuthInfo): void {
4650
if (!WebSocket) {
4751
console.log('Your browser doesn\'t support WebSockets.')
4852
return
4953
}
5054

55+
this.auth = auth
56+
5157
let authSent = false
5258
this.socketClient = new SocketIoClient()
5359

@@ -64,7 +70,7 @@ export default class LeanplumSocket {
6470
}
6571
}
6672

67-
this.socketClient.onerror = (event) => {
73+
this.socketClient.onerror = (event: string) => {
6874
console.log('Leanplum: Socket error', event)
6975
}
7076

@@ -86,13 +92,18 @@ export default class LeanplumSocket {
8692

8793
public setSocketHost(value: string): void {
8894
this.socketHost = value
95+
96+
if (this.socketClient.connected) {
97+
this.socketClient.disconnect()
98+
this.connect(this.auth)
99+
}
89100
}
90101

91102
/**
92103
* Sets the network timeout.
93-
* @param {number} seconds The timeout in seconds.
104+
* @param seconds The timeout in seconds.
94105
*/
95-
public setNetworkTimeout(seconds): void {
106+
public setNetworkTimeout(seconds: number): void {
96107
this.networkTimeoutSeconds = seconds
97108
this.socketClient?.setNetworkTimeout(seconds)
98109
}

src/Network.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default class Network {
7171
handled = true
7272

7373
let response
74-
let ranCallback = false
74+
let parseError = false
7575
if (plainText) {
7676
response = xhr.responseText
7777
} else {
@@ -83,11 +83,11 @@ export default class Network {
8383
error(null, xhr)
8484
}
8585
}, 0)
86-
ranCallback = true
86+
parseError = true
8787
}
8888
}
8989

90-
if (!ranCallback) {
90+
if (!parseError) {
9191
if (xhr.status >= 200 && xhr.status < 300) {
9292
setTimeout(() => {
9393
if (success) {
@@ -133,7 +133,7 @@ export default class Network {
133133
const xdr = new XDomainRequest()
134134
xdr.onload = () => {
135135
let response
136-
let ranCallback = false
136+
let parseError = false
137137
if (plainText) {
138138
response = xdr.responseText
139139
} else {
@@ -145,10 +145,10 @@ export default class Network {
145145
error(null, xdr)
146146
}
147147
}, 0)
148-
ranCallback = true
148+
parseError = true
149149
}
150150
}
151-
if (!ranCallback) {
151+
if (!parseError) {
152152
setTimeout(() => {
153153
if (success) {
154154
success(response, xdr)
@@ -179,7 +179,7 @@ export default class Network {
179179

180180
/**
181181
* Adds the request to the request queue.
182-
* @param {Arguments} requestArguments The request arguments from the initial method call.
182+
* @param requestArguments The request arguments from the initial method call.
183183
* @private
184184
*/
185185
private enqueueRequest(requestArguments): void {

src/SocketIoClient.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ export default class SocketIoClient {
4141

4242
/**
4343
* Connects to the given socketHost.
44-
* @param {string} socketHost The host to connect to.
44+
* @param socketHost The host to connect to.
4545
*/
46-
public connect(socketHost): void {
46+
public connect(socketHost: string): void {
4747
this.connecting = true
4848
this.network.ajax('POST', `https://${socketHost}/socket.io/1`, '',
49-
(line) => {
49+
(line: string) => {
5050
const parts = line.split(':')
5151
const session = parts[0]
5252
const heartbeat = parseInt(parts[1]) / 2 * 1000
@@ -99,12 +99,18 @@ export default class SocketIoClient {
9999
)
100100
}
101101

102+
public disconnect(): void {
103+
this.socket.close()
104+
this.connected = false
105+
this.connecting = false
106+
}
107+
102108
/**
103109
* Sends given event with arguments to the server.
104-
* @param {string} name Name of the event.
105-
* @param {*} args Arguments to send.
110+
* @param name Name of the event.
111+
* @param args Arguments to send.
106112
*/
107-
public send(name, args): void {
113+
public send(name: string, args): void {
108114
if (!this.connected) {
109115
console.log('Leanplum: Socket is not connected.')
110116
return
@@ -118,9 +124,9 @@ export default class SocketIoClient {
118124

119125
/**
120126
* Sets the network timeout.
121-
* @param {number} seconds The timeout in seconds.
127+
* @param The timeout in seconds.
122128
*/
123-
public setNetworkTimeout(seconds): void {
129+
public setNetworkTimeout(seconds: number): void {
124130
this.network.setNetworkTimeout(seconds)
125131
}
126132
}

test/mocks/internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const lpRequestMock: Partial<jest.Mocked<LeanplumRequest>> = {
1111

1212
export const lpSocketMock = {
1313
connect: jest.fn(),
14+
setSocketHost: jest.fn(),
1415
}
1516

1617
export const pushManagerMock: Partial<jest.Mocked<PushManager>> = {

test/specs/LeanplumInternal.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe(LeanplumInternal, () => {
4343
it('passes available message IDs to API', () => {
4444
const inbox = lp.inbox()
4545
lpRequestMock.request.mockImplementationOnce(
46-
(method, args, options) => {
46+
(_method, _args, options) => {
4747
options.response({
4848
success: true,
4949
response: [ { newsfeedMessages: {
@@ -921,6 +921,17 @@ describe(LeanplumInternal, () => {
921921
expect(varCacheMock.clearUserContent).toHaveBeenCalledTimes(1)
922922
})
923923
})
924+
925+
describe('devserver host updates', () => {
926+
it('reconnects to new host on update', () => {
927+
jest.spyOn(lp, 'setSocketHost');
928+
929+
(lp as any)._events.emit('updateDevServerHost', 'dev2.leanplum.com')
930+
931+
expect(lp.setSocketHost).toHaveBeenCalledTimes(1)
932+
expect(lp.setSocketHost).toHaveBeenCalledWith('dev2.leanplum.com')
933+
})
934+
})
924935
})
925936

926937
describe('getFileUrl', () => {

0 commit comments

Comments
 (0)