Skip to content

Commit 5766c1a

Browse files
authored
feat(datafile manager): Update datafile manager interface to work with objects instead of strings. Add StaticDatafileManager. (#9)
Summary: - DatafileManager interface uses objects instead of strings - HttpPollingDatafileManager uses JSON.parse on response bodies - Added StaticDatafileManager as a top level export (will be used in optimizely-sdk) Test plan: Updated unit tests & added new unit tests. Issues: https://optimizely.atlassian.net/browse/OASIS-4384
1 parent 82e4d0a commit 5766c1a

File tree

8 files changed

+126
-24
lines changed

8 files changed

+126
-24
lines changed

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

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ describe('httpPollingDatafileManager', () => {
8181

8282
describe('when constructed with sdkKey and datafile', () => {
8383
beforeEach(() => {
84-
manager = createTestManager({ datafile: 'abcd', sdkKey: '123' })
84+
manager = createTestManager({ datafile: { foo: 'abcd' }, sdkKey: '123' })
8585
})
8686

8787
it('returns the passed datafile from get', () => {
88-
expect(manager.get()).toBe('abcd')
88+
expect(manager.get()).toEqual({ foo: 'abcd' })
8989
})
9090

9191
it('after being started, fetches the datafile and resolves onReady', async () => {
@@ -96,7 +96,7 @@ describe('httpPollingDatafileManager', () => {
9696
})
9797
manager.start()
9898
await manager.onReady()
99-
expect(manager.get()).toBe('{"foo": "bar"}')
99+
expect(manager.get()).toEqual({ foo: 'bar' })
100100
})
101101
})
102102

@@ -133,7 +133,27 @@ describe('httpPollingDatafileManager', () => {
133133
})
134134
manager.start()
135135
await manager.onReady()
136-
expect(manager.get()).toBe('{"foo": "bar"}')
136+
expect(manager.get()).toEqual({ foo: 'bar' })
137+
})
138+
139+
it('does not update if the response body is not valid json', async () => {
140+
manager.queuedResponses.push(
141+
{
142+
statusCode: 200,
143+
body: '{"foo" "',
144+
headers: {},
145+
},
146+
{
147+
statusCode: 200,
148+
body: '{"foo": "bar"}',
149+
headers: {},
150+
},
151+
)
152+
manager.start()
153+
await manager.onReady()
154+
testTimeoutFactory.timeoutFns[0]()
155+
await manager.responsePromises[1]
156+
expect(manager.get()).toEqual({ foo: 'bar' })
137157
})
138158

139159
describe('live updates', () => {
@@ -176,22 +196,22 @@ describe('httpPollingDatafileManager', () => {
176196

177197
manager.start()
178198
await manager.onReady()
179-
expect(manager.get()).toBe('{"foo": "bar"}')
199+
expect(manager.get()).toEqual({ foo: 'bar' })
180200
expect(updateFn).toBeCalledTimes(0)
181201

182202
testTimeoutFactory.timeoutFns[0]()
183203
await manager.responsePromises[1]
184204
expect(updateFn).toBeCalledTimes(1)
185-
expect(updateFn.mock.calls[0][0]).toEqual({ datafile: '{"foo2": "bar2"}' })
186-
expect(manager.get()).toBe('{"foo2": "bar2"}')
205+
expect(updateFn.mock.calls[0][0]).toEqual({ datafile: { foo2: 'bar2' } })
206+
expect(manager.get()).toEqual({ foo2: 'bar2' })
187207

188208
updateFn.mockReset()
189209

190210
testTimeoutFactory.timeoutFns[1]()
191211
await manager.responsePromises[2]
192212
expect(updateFn).toBeCalledTimes(1)
193-
expect(updateFn.mock.calls[0][0]).toEqual({ datafile: '{"foo3": "bar3"}' })
194-
expect(manager.get()).toBe('{"foo3": "bar3"}')
213+
expect(updateFn.mock.calls[0][0]).toEqual({ datafile: { foo3: 'bar3' } })
214+
expect(manager.get()).toEqual({ foo3: 'bar3' })
195215
})
196216

197217
it('cancels a pending timeout when stop is called', async () => {
@@ -228,13 +248,13 @@ describe('httpPollingDatafileManager', () => {
228248

229249
manager.start()
230250
await manager.onReady()
231-
expect(manager.get()).toBe('{"foo": "bar"}')
251+
expect(manager.get()).toEqual({ foo: 'bar' })
232252
testTimeoutFactory.timeoutFns[0]()
233253
expect(manager.responsePromises.length).toBe(2)
234254
manager.stop()
235255
await manager.responsePromises[1]
236256
// Should not have updated datafile since manager was stopped
237-
expect(manager.get()).toBe('{"foo": "bar"}')
257+
expect(manager.get()).toEqual({ foo: 'bar' })
238258
})
239259

240260
it('calls abort on the current request if there is a current request when stop is called', async () => {
@@ -276,7 +296,7 @@ describe('httpPollingDatafileManager', () => {
276296
// Trigger the update, should fetch the next response which should succeed, then we get ready
277297
testTimeoutFactory.timeoutFns[0]()
278298
await manager.onReady()
279-
expect(manager.get()).toBe('{"foo": "bar"}')
299+
expect(manager.get()).toEqual({ foo: 'bar' })
280300
})
281301

282302
describe('newness checking', () => {
@@ -301,7 +321,7 @@ describe('httpPollingDatafileManager', () => {
301321

302322
manager.start()
303323
await manager.onReady()
304-
expect(manager.get()).toBe('{"foo": "bar"}')
324+
expect(manager.get()).toEqual({ foo: 'bar' })
305325
// First response promise was for the initial 200 response
306326
expect(manager.responsePromises.length).toBe(1)
307327
// Trigger the queued update
@@ -311,7 +331,7 @@ describe('httpPollingDatafileManager', () => {
311331
await manager.responsePromises[1]
312332
// Since the response was 304, updateFn should not have been called
313333
expect(updateFn).toBeCalledTimes(0)
314-
expect(manager.get()).toBe('{"foo": "bar"}')
334+
expect(manager.get()).toEqual({ foo: 'bar' })
315335
})
316336

317337
it('sends if-modified-since using the last observed response last-modified', async () => {
@@ -431,7 +451,7 @@ describe('httpPollingDatafileManager', () => {
431451
})
432452
manager.start()
433453
await manager.onReady()
434-
expect(manager.get()).toBe('{"foo": "bar"}')
454+
expect(manager.get()).toEqual({ foo: 'bar' })
435455
})
436456

437457
it('does not schedule a live update after ready', async () => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import StaticDatafileManager from '../src/staticDatafileManager'
2+
3+
describe('staticDatafileManager', () => {
4+
it('can be constructed with a datafile object and become ready', async () => {
5+
const manager = new StaticDatafileManager({ foo: 'bar' })
6+
manager.start()
7+
await manager.onReady()
8+
})
9+
10+
it('returns the datafile it was constructed with from get', async () => {
11+
const manager = new StaticDatafileManager({ foo: 'bar' })
12+
manager.start()
13+
expect(manager.get()).toEqual({ foo: 'bar' })
14+
await manager.onReady()
15+
expect(manager.get()).toEqual({ foo: 'bar' })
16+
})
17+
18+
it('can be stopped', async () => {
19+
const manager = new StaticDatafileManager({ foo: 'bar' })
20+
manager.start()
21+
await manager.onReady()
22+
await manager.stop()
23+
})
24+
25+
it('can have event listeners added', () => {
26+
const manager = new StaticDatafileManager({ foo: 'bar' })
27+
const dispose = manager.on('update', jest.fn())
28+
dispose()
29+
})
30+
})

packages/datafile-manager/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "datafile-manager",
2+
"name": "@optimizely/js-sdk-datafile-manager",
33
"version": "0.1.0",
44
"description": "Optimizely Full Stack Datafile Manager",
55
"homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/datafile-manager",
@@ -50,6 +50,6 @@
5050
},
5151
"scripts": {
5252
"test": "jest",
53-
"build": "rm -r lib && tsc"
53+
"tsc": "rm -rf lib && tsc"
5454
}
5555
}

packages/datafile-manager/src/datafileManager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { TimeoutFactory } from "./timeoutFactory";
1717
*/
1818

1919
export interface DatafileUpdate {
20-
datafile: string
20+
datafile: object
2121
}
2222

2323
export interface DatafileUpdateListener {
@@ -32,7 +32,7 @@ interface Managed {
3232
}
3333

3434
export interface DatafileManager extends Managed {
35-
get: () => string | null
35+
get: () => object | null
3636
on: (eventName: string, listener: DatafileUpdateListener) => () => void
3737
onReady: () => Promise<void>
3838
}
@@ -46,7 +46,7 @@ export enum CacheDirective {
4646

4747
export interface DatafileManagerConfig {
4848
autoUpdate?: boolean
49-
datafile?: string
49+
datafile?: object
5050
sdkKey: string
5151
timeoutFactory?: TimeoutFactory,
5252
updateInterval?: number

packages/datafile-manager/src/httpPollingDatafileManager.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
4444
// Return any default configuration options that should be applied
4545
protected abstract getConfigDefaults(): Partial<DatafileManagerConfig>
4646

47-
private currentDatafile: string | null
47+
private currentDatafile: object | null
4848

4949
private readonly sdkKey: string
5050

@@ -125,7 +125,7 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
125125
this.backoffController = new BackoffController()
126126
}
127127

128-
get(): string | null {
128+
get(): object | null {
129129
return this.currentDatafile
130130
}
131131

@@ -273,7 +273,7 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
273273
}, nextUpdateDelay)
274274
}
275275

276-
private getNextDatafileFromResponse(response: Response): string | null {
276+
private getNextDatafileFromResponse(response: Response): object | null {
277277
logger.debug('Response status code: %s', response.statusCode)
278278
if (typeof response.statusCode === 'undefined') {
279279
return null
@@ -282,11 +282,28 @@ export default abstract class HTTPPollingDatafileManager implements DatafileMana
282282
return null
283283
}
284284
if (isSuccessStatusCode(response.statusCode)) {
285-
return response.body
285+
return this.tryParsingBodyAsJSON(response.body);
286286
}
287287
return null
288288
}
289289

290+
private tryParsingBodyAsJSON(body: string): object | null {
291+
let parseResult: any
292+
try {
293+
parseResult = JSON.parse(body)
294+
} catch (err) {
295+
logger.error('Error parsing response body: %s', err.message, err)
296+
return null
297+
}
298+
let datafileObj: object | null = null
299+
if (typeof parseResult === 'object' && parseResult !== null) {
300+
datafileObj = parseResult
301+
} else {
302+
logger.error('Error parsing response body: was not an object')
303+
}
304+
return datafileObj
305+
}
306+
290307
private trySavingLastModified(headers: Headers): void {
291308
const lastModifiedHeader = headers['last-modified'] || headers['Last-Modified']
292309
if (typeof lastModifiedHeader !== 'undefined') {

packages/datafile-manager/src/index.browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export * from './datafileManager'
1818
export { default as DatafileManager } from './browserDatafileManager'
19+
export { default as StaticDatafileManager } from './staticDatafileManager';

packages/datafile-manager/src/index.node.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export * from './datafileManager'
1818
export { default as DatafileManager } from './nodeDatafileManager'
19+
export { default as StaticDatafileManager } from './staticDatafileManager';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { DatafileManager, DatafileUpdate } from './datafileManager';
2+
3+
const doNothing = () => {};
4+
5+
export default class StaticDatafileManager implements DatafileManager {
6+
private readonly datafile: object | null
7+
8+
private readyPromise: Promise<void>
9+
10+
constructor(datafile: object | null) {
11+
this.datafile = datafile
12+
this.readyPromise = Promise.resolve();
13+
}
14+
15+
get() {
16+
return this.datafile
17+
}
18+
19+
onReady() {
20+
return this.readyPromise
21+
}
22+
23+
start() {
24+
}
25+
26+
stop() {
27+
return Promise.resolve();
28+
}
29+
30+
on(eventName: string, listener: (datafileUpdate: DatafileUpdate) => void) {
31+
return doNothing
32+
}
33+
}

0 commit comments

Comments
 (0)