Skip to content

Commit 8a9cc00

Browse files
committed
basic setup
1 parent 843c9c4 commit 8a9cc00

File tree

7 files changed

+5142
-25
lines changed

7 files changed

+5142
-25
lines changed

packages/datafile-manager/src/httpPollingDatafileManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana
157157
}
158158

159159
start(): void {
160+
console.log('------ INSIDE THE LOCAL DATAFILE MANAGER 2 ------');
160161
if (!this.isStarted) {
161162
logger.debug('Datafile manager started');
162163
this.isStarted = true;
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
/**
2+
* Copyright 2019-2020, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { getLogger } from '@optimizely/js-sdk-logging';
18+
import { sprintf } from '@optimizely/js-sdk-utils';
19+
import { DatafileManager, DatafileManagerConfig, DatafileUpdate } from './datafileManager';
20+
import EventEmitter, { Disposer } from './eventEmitter';
21+
import { AbortableRequest, Response, Headers } from './http';
22+
import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE } from './config';
23+
import BackoffController from './backoffController';
24+
import PersistentKeyValueCache from './persistentKeyValueCache';
25+
26+
const logger = getLogger('DatafileManager');
27+
28+
const UPDATE_EVT = 'update';
29+
30+
function isValidUpdateInterval(updateInterval: number): boolean {
31+
return updateInterval >= MIN_UPDATE_INTERVAL;
32+
}
33+
34+
function isSuccessStatusCode(statusCode: number): boolean {
35+
return statusCode >= 200 && statusCode < 400;
36+
}
37+
38+
const noOpKeyValueCache: PersistentKeyValueCache = {
39+
get(): Promise<string> {
40+
return Promise.resolve('');
41+
},
42+
43+
set(): Promise<void> {
44+
return Promise.resolve();
45+
},
46+
47+
contains(): Promise<boolean> {
48+
return Promise.resolve(false);
49+
},
50+
51+
remove(): Promise<void> {
52+
return Promise.resolve();
53+
},
54+
};
55+
56+
export default abstract class HttpPollingDatafileManager implements DatafileManager {
57+
// Make an HTTP get request to the given URL with the given headers
58+
// Return an AbortableRequest, which has a promise for a Response.
59+
// If we can't get a response, the promise is rejected.
60+
// The request will be aborted if the manager is stopped while the request is in flight.
61+
protected abstract makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest;
62+
63+
// Return any default configuration options that should be applied
64+
protected abstract getConfigDefaults(): Partial<DatafileManagerConfig>;
65+
66+
private currentDatafile: string;
67+
68+
private readonly readyPromise: Promise<void>;
69+
70+
private isReadyPromiseSettled: boolean;
71+
72+
private readyPromiseResolver: () => void;
73+
74+
private readyPromiseRejecter: (err: Error) => void;
75+
76+
private readonly emitter: EventEmitter;
77+
78+
private readonly autoUpdate: boolean;
79+
80+
private readonly updateInterval: number;
81+
82+
private currentTimeout: any;
83+
84+
private isStarted: boolean;
85+
86+
private lastResponseLastModified?: string;
87+
88+
private datafileUrl: string;
89+
90+
private currentRequest: AbortableRequest | null;
91+
92+
private backoffController: BackoffController;
93+
94+
private cacheKey: string;
95+
96+
private cache: PersistentKeyValueCache;
97+
98+
// When true, this means the update interval timeout fired before the current
99+
// sync completed. In that case, we should sync again immediately upon
100+
// completion of the current request, instead of waiting another update
101+
// interval.
102+
private syncOnCurrentRequestComplete: boolean;
103+
104+
constructor(config: DatafileManagerConfig) {
105+
const configWithDefaultsApplied: DatafileManagerConfig = {
106+
...this.getConfigDefaults(),
107+
...config,
108+
};
109+
const {
110+
datafile,
111+
autoUpdate = false,
112+
sdkKey,
113+
updateInterval = DEFAULT_UPDATE_INTERVAL,
114+
urlTemplate = DEFAULT_URL_TEMPLATE,
115+
cache = noOpKeyValueCache,
116+
} = configWithDefaultsApplied;
117+
118+
this.cache = cache;
119+
this.cacheKey = 'opt-datafile-' + sdkKey;
120+
this.isReadyPromiseSettled = false;
121+
this.readyPromiseResolver = (): void => {};
122+
this.readyPromiseRejecter = (): void => {};
123+
this.readyPromise = new Promise((resolve, reject) => {
124+
this.readyPromiseResolver = resolve;
125+
this.readyPromiseRejecter = reject;
126+
});
127+
128+
if (datafile) {
129+
this.currentDatafile = datafile;
130+
if (!sdkKey) {
131+
this.resolveReadyPromise();
132+
}
133+
} else {
134+
this.currentDatafile = '';
135+
}
136+
137+
this.isStarted = false;
138+
139+
this.datafileUrl = sprintf(urlTemplate, sdkKey);
140+
141+
this.emitter = new EventEmitter();
142+
this.autoUpdate = autoUpdate;
143+
if (isValidUpdateInterval(updateInterval)) {
144+
this.updateInterval = updateInterval;
145+
} else {
146+
logger.warn('Invalid updateInterval %s, defaulting to %s', updateInterval, DEFAULT_UPDATE_INTERVAL);
147+
this.updateInterval = DEFAULT_UPDATE_INTERVAL;
148+
}
149+
this.currentTimeout = null;
150+
this.currentRequest = null;
151+
this.backoffController = new BackoffController();
152+
this.syncOnCurrentRequestComplete = false;
153+
}
154+
155+
get(): string {
156+
return this.currentDatafile;
157+
}
158+
159+
start(): void {
160+
console.log('------ INSIDE THE REAL TIME DATAFILE MANAGER 2 ------');
161+
if (!this.isStarted) {
162+
logger.debug('Datafile manager started');
163+
this.isStarted = true;
164+
this.backoffController.reset();
165+
this.setDatafileFromCacheIfAvailable();
166+
this.syncDatafile();
167+
}
168+
}
169+
170+
stop(): Promise<void> {
171+
logger.debug('Datafile manager stopped');
172+
this.isStarted = false;
173+
if (this.currentTimeout) {
174+
clearTimeout(this.currentTimeout);
175+
this.currentTimeout = null;
176+
}
177+
178+
this.emitter.removeAllListeners();
179+
180+
if (this.currentRequest) {
181+
this.currentRequest.abort();
182+
this.currentRequest = null;
183+
}
184+
185+
return Promise.resolve();
186+
}
187+
188+
onReady(): Promise<void> {
189+
return this.readyPromise;
190+
}
191+
192+
on(eventName: string, listener: (datafileUpdate: DatafileUpdate) => void): Disposer {
193+
return this.emitter.on(eventName, listener);
194+
}
195+
196+
private onRequestRejected(err: any): void {
197+
if (!this.isStarted) {
198+
return;
199+
}
200+
201+
this.backoffController.countError();
202+
203+
if (err instanceof Error) {
204+
logger.error('Error fetching datafile: %s', err.message, err);
205+
} else if (typeof err === 'string') {
206+
logger.error('Error fetching datafile: %s', err);
207+
} else {
208+
logger.error('Error fetching datafile');
209+
}
210+
}
211+
212+
private onRequestResolved(response: Response): void {
213+
if (!this.isStarted) {
214+
return;
215+
}
216+
217+
if (typeof response.statusCode !== 'undefined' && isSuccessStatusCode(response.statusCode)) {
218+
this.backoffController.reset();
219+
} else {
220+
this.backoffController.countError();
221+
}
222+
223+
this.trySavingLastModified(response.headers);
224+
225+
const datafile = this.getNextDatafileFromResponse(response);
226+
if (datafile !== '') {
227+
logger.info('Updating datafile from response');
228+
this.currentDatafile = datafile;
229+
this.cache.set(this.cacheKey, datafile);
230+
if (!this.isReadyPromiseSettled) {
231+
this.resolveReadyPromise();
232+
} else {
233+
const datafileUpdate: DatafileUpdate = {
234+
datafile,
235+
};
236+
this.emitter.emit(UPDATE_EVT, datafileUpdate);
237+
}
238+
}
239+
}
240+
241+
private onRequestComplete(this: HttpPollingDatafileManager): void {
242+
if (!this.isStarted) {
243+
return;
244+
}
245+
246+
this.currentRequest = null;
247+
248+
if (!this.isReadyPromiseSettled && !this.autoUpdate) {
249+
// We will never resolve ready, so reject it
250+
this.rejectReadyPromise(new Error('Failed to become ready'));
251+
}
252+
253+
if (this.autoUpdate && this.syncOnCurrentRequestComplete) {
254+
this.syncDatafile();
255+
}
256+
this.syncOnCurrentRequestComplete = false;
257+
}
258+
259+
private syncDatafile(): void {
260+
const headers: Headers = {};
261+
if (this.lastResponseLastModified) {
262+
headers['if-modified-since'] = this.lastResponseLastModified;
263+
}
264+
265+
logger.debug('Making datafile request to url %s with headers: %s', this.datafileUrl, () => JSON.stringify(headers));
266+
this.currentRequest = this.makeGetRequest(this.datafileUrl, headers);
267+
268+
const onRequestComplete = (): void => {
269+
this.onRequestComplete();
270+
};
271+
const onRequestResolved = (response: Response): void => {
272+
this.onRequestResolved(response);
273+
};
274+
const onRequestRejected = (err: any): void => {
275+
this.onRequestRejected(err);
276+
};
277+
this.currentRequest.responsePromise
278+
.then(onRequestResolved, onRequestRejected)
279+
.then(onRequestComplete, onRequestComplete);
280+
281+
if (this.autoUpdate) {
282+
this.scheduleNextUpdate();
283+
}
284+
}
285+
286+
private resolveReadyPromise(): void {
287+
this.readyPromiseResolver();
288+
this.isReadyPromiseSettled = true;
289+
}
290+
291+
private rejectReadyPromise(err: Error): void {
292+
this.readyPromiseRejecter(err);
293+
this.isReadyPromiseSettled = true;
294+
}
295+
296+
private scheduleNextUpdate(): void {
297+
const currentBackoffDelay = this.backoffController.getDelay();
298+
const nextUpdateDelay = Math.max(currentBackoffDelay, this.updateInterval);
299+
logger.debug('Scheduling sync in %s ms', nextUpdateDelay);
300+
this.currentTimeout = setTimeout(() => {
301+
if (this.currentRequest) {
302+
this.syncOnCurrentRequestComplete = true;
303+
} else {
304+
this.syncDatafile();
305+
}
306+
}, nextUpdateDelay);
307+
}
308+
309+
private getNextDatafileFromResponse(response: Response): string {
310+
logger.debug('Response status code: %s', response.statusCode);
311+
if (typeof response.statusCode === 'undefined') {
312+
return '';
313+
}
314+
if (response.statusCode === 304) {
315+
return '';
316+
}
317+
if (isSuccessStatusCode(response.statusCode)) {
318+
return response.body;
319+
}
320+
return '';
321+
}
322+
323+
private trySavingLastModified(headers: Headers): void {
324+
const lastModifiedHeader = headers['last-modified'] || headers['Last-Modified'];
325+
if (typeof lastModifiedHeader !== 'undefined') {
326+
this.lastResponseLastModified = lastModifiedHeader;
327+
logger.debug('Saved last modified header value from response: %s', this.lastResponseLastModified);
328+
}
329+
}
330+
331+
setDatafileFromCacheIfAvailable(): void {
332+
this.cache.get(this.cacheKey).then(datafile => {
333+
if (this.isStarted && !this.isReadyPromiseSettled && datafile !== '') {
334+
logger.debug('Using datafile from cache');
335+
this.currentDatafile = datafile;
336+
this.resolveReadyPromise();
337+
}
338+
});
339+
}
340+
}

packages/optimizely-sdk/lib/index.browser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ let hasRetriedEvents = false;
5252
* null on error
5353
*/
5454
const createInstance = function(config: Config): Client | null {
55+
console.log('------ IN BROWSER ENTRY POINT ------');
56+
const isRealtime = config.enableRealtimeUpdateNotification;
57+
const isStreaming = !isRealtime && config.enableStreaming;
5558
try {
5659
// TODO warn about setting per instance errorHandler / logger / logLevel
5760
let isValidInstance = false

packages/optimizely-sdk/lib/shared_types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,11 @@ export interface TrackListenerPayload extends ListenerPayload {
383383
eventMaxQueueSize?: number;
384384
// sdk key
385385
sdkKey?: string;
386+
// Enables real time datafile update notification pushed in real time from the backend
387+
enableRealtimeUpdateNotification?: boolean;
388+
// Enables streaming of datafile updates as they occur.
389+
// When `enableRealtimeUpdateNotification` and `enableStreaming` are both true, `enableRealtimeUpdateNotification` will take effect.
390+
enableStreaming?: boolean;
386391
}
387392

388393
/**

0 commit comments

Comments
 (0)