Skip to content

Commit 1c80d37

Browse files
authored
feat (datafile manager): Abortable requests, abort on stop (#2)
Summary: Update expected return value of abstract method `makeGetRequest`. Should return a request object with an `abort` method. This will be useful in the Node datafile manager for aborting the request on `stop`. Test plan: Updated unit tests Issues: https://optimizely.atlassian.net/browse/OASIS-4309
1 parent 5fc68f3 commit 1c80d37

File tree

3 files changed

+49
-16
lines changed

3 files changed

+49
-16
lines changed

packages/datafile-manager/__test__/httpPollingDatafileManager.spec.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import HTTPPollingDatafileManager from '../src/httpPollingDatafileManager'
18-
import { Headers, Response } from '../src/http'
18+
import { Headers, AbortableRequest, Response } from '../src/http'
1919
import { TimeoutFactory } from '../src/timeoutFactory'
2020
import { DatafileManagerConfig } from '../src/datafileManager';
2121

@@ -46,14 +46,16 @@ class TestDatafileManager extends HTTPPollingDatafileManager {
4646

4747
responsePromises: Promise<Response>[] = []
4848

49-
makeGetRequest(url: string, headers: Headers): Promise<Response> {
49+
makeGetRequest(url: string, headers: Headers): AbortableRequest {
5050
const nextResponse: Response | undefined = this.queuedResponses.pop()
51+
let responsePromise: Promise<Response>
5152
if (nextResponse === undefined) {
52-
return Promise.reject('No responses queued')
53+
responsePromise = Promise.reject('No responses queued')
54+
} else {
55+
responsePromise = Promise.resolve(nextResponse)
5356
}
54-
const respPromise = Promise.resolve(nextResponse)
55-
this.responsePromises.push(respPromise)
56-
return respPromise
57+
this.responsePromises.push(responsePromise)
58+
return { responsePromise, abort: jest.fn() }
5759
}
5860
}
5961

@@ -103,10 +105,6 @@ describe('httpPollingDatafileManager', () => {
103105
manager = createTestManager({ sdkKey: '123', updateInterval: 10 })
104106
})
105107

106-
afterEach(() => {
107-
manager.stop()
108-
})
109-
110108
describe('initial state', () => {
111109
it('returns null from get before becoming ready', () => {
112110
expect(manager.get()).toBeNull()
@@ -239,6 +237,23 @@ describe('httpPollingDatafileManager', () => {
239237
expect(manager.get()).toBe('{"foo": "bar"}')
240238
})
241239

240+
it('calls abort on the current request if there is a current request when stop is called', async () => {
241+
manager.queuedResponses.push(
242+
{
243+
statusCode: 200,
244+
body: '{"foo2": "bar2"}',
245+
headers: {},
246+
}
247+
)
248+
const makeGetRequestSpy = jest.spyOn(manager, 'makeGetRequest')
249+
manager.start()
250+
const currentRequest = makeGetRequestSpy.mock.results[0]
251+
expect(currentRequest.type).toBe('return')
252+
expect(currentRequest.value.abort).toBeCalledTimes(0)
253+
manager.stop()
254+
expect(currentRequest.value.abort).toBeCalledTimes(1)
255+
})
256+
242257
it('can fail to become ready on the initial request, but succeed after a later polling update', async () => {
243258
manager.queuedResponses.push(
244259
{
@@ -366,7 +381,7 @@ describe('httpPollingDatafileManager', () => {
366381
body: '{"foo": "bar"}',
367382
headers: {},
368383
})
369-
manager.makeGetRequest = () => Promise.reject(new Error('Could not connect'))
384+
manager.makeGetRequest = () => ({ abort() {}, responsePromise: Promise.reject(new Error('Could not connect')) })
370385
manager.start()
371386
let didReject = false
372387
try {

packages/datafile-manager/src/http.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ export interface Response {
2323
body: string
2424
headers: Headers
2525
}
26+
27+
export interface AbortableRequest {
28+
abort(): void
29+
responsePromise: Promise<Response>
30+
}

packages/datafile-manager/src/httpPollingDatafileManager.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { getLogger } from '@optimizely/js-sdk-logging'
1818
import { DatafileManager, DatafileManagerConfig, DatafileUpdate } from './datafileManager';
1919
import EventEmitter from './eventEmitter'
20-
import { Response, Headers } from './http';
20+
import { AbortableRequest, Response, Headers } from './http';
2121
import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE, SDK_KEY_TOKEN } from './config'
2222
import { TimeoutFactory, DEFAULT_TIMEOUT_FACTORY } from './timeoutFactory'
2323

@@ -31,8 +31,10 @@ function isValidUpdateInterval(updateInterval: number): boolean {
3131

3232
export default abstract class HTTPPollingDatafileManager implements DatafileManager {
3333
// Make an HTTP get request to the given URL with the given headers
34-
// Return a promise for a Response. If we can't get a response, the promise is rejected
35-
protected abstract makeGetRequest(reqUrl: string, headers: Headers): Promise<Response>
34+
// Return an AbortableRequest, which has a promise for a Response.
35+
// If we can't get a response, the promise is rejected.
36+
// The request will be aborted if the manager is stopped while the request is in flight.
37+
protected abstract makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest
3638

3739
public readonly onReady: Promise<void>
3840

@@ -62,6 +64,8 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
6264

6365
private timeoutFactory: TimeoutFactory
6466

67+
private currentRequest?: AbortableRequest
68+
6569
constructor(config: DatafileManagerConfig) {
6670
const {
6771
datafile,
@@ -125,7 +129,14 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
125129
this.cancelTimeout()
126130
this.cancelTimeout = undefined
127131
}
132+
128133
this.emitter.removeAllListeners()
134+
135+
if (this.currentRequest) {
136+
this.currentRequest.abort()
137+
this.currentRequest = undefined
138+
}
139+
129140
return Promise.resolve()
130141
}
131142

@@ -178,6 +189,8 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
178189
return
179190
}
180191

192+
this.currentRequest = undefined
193+
181194
if (this.liveUpdates) {
182195
this.scheduleNextUpdate()
183196
}
@@ -196,7 +209,7 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
196209
const datafileUrl = this.getUrl(this.sdkKey)
197210

198211
logger.debug('Making datafile request to url %s with headers: %s', datafileUrl, () => JSON.stringify(headers))
199-
const datafileFetch = this.makeGetRequest(datafileUrl, headers)
212+
this.currentRequest = this.makeGetRequest(datafileUrl, headers)
200213

201214
const onFetchComplete = () => {
202215
this.onFetchComplete()
@@ -207,7 +220,7 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
207220
const logMakeGetRequestError = (err: any) => {
208221
this.logMakeGetRequestError(err)
209222
}
210-
datafileFetch
223+
this.currentRequest.responsePromise
211224
.then(tryUpdatingDatafile, logMakeGetRequestError)
212225
.then(onFetchComplete, onFetchComplete)
213226
}

0 commit comments

Comments
 (0)