-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement support for browser requests. (#578)
- Loading branch information
1 parent
fe82500
commit 887548a
Showing
10 changed files
with
376 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import Backoff from '../../src/platform/Backoff'; | ||
|
||
const noJitter = (): number => 0; | ||
const maxJitter = (): number => 1; | ||
const defaultResetInterval = 60 * 1000; | ||
|
||
it.each([1, 1000, 5000])('has the correct starting delay', (initialDelay) => { | ||
const backoff = new Backoff(initialDelay, defaultResetInterval, noJitter); | ||
expect(backoff.fail()).toEqual(initialDelay); | ||
}); | ||
|
||
it.each([1, 1000, 5000])('doubles delay on consecutive failures', (initialDelay) => { | ||
const backoff = new Backoff(initialDelay, defaultResetInterval, noJitter); | ||
expect(backoff.fail()).toEqual(initialDelay); | ||
expect(backoff.fail()).toEqual(initialDelay * 2); | ||
expect(backoff.fail()).toEqual(initialDelay * 4); | ||
}); | ||
|
||
it('stops increasing delay when the max backoff is encountered', () => { | ||
const backoff = new Backoff(5000, defaultResetInterval, noJitter); | ||
expect(backoff.fail()).toEqual(5000); | ||
expect(backoff.fail()).toEqual(10000); | ||
expect(backoff.fail()).toEqual(20000); | ||
expect(backoff.fail()).toEqual(30000); | ||
|
||
const backoff2 = new Backoff(1000, defaultResetInterval, noJitter); | ||
expect(backoff2.fail()).toEqual(1000); | ||
expect(backoff2.fail()).toEqual(2000); | ||
expect(backoff2.fail()).toEqual(4000); | ||
expect(backoff2.fail()).toEqual(8000); | ||
expect(backoff2.fail()).toEqual(16000); | ||
expect(backoff2.fail()).toEqual(30000); | ||
}); | ||
|
||
it('handles an initial retry delay longer than the maximum retry delay', () => { | ||
const backoff = new Backoff(40000, defaultResetInterval, noJitter); | ||
expect(backoff.fail()).toEqual(30000); | ||
}); | ||
|
||
it('jitters the backoff value', () => { | ||
const backoff = new Backoff(1000, defaultResetInterval, maxJitter); | ||
expect(backoff.fail()).toEqual(500); | ||
expect(backoff.fail()).toEqual(1000); | ||
expect(backoff.fail()).toEqual(2000); | ||
expect(backoff.fail()).toEqual(4000); | ||
expect(backoff.fail()).toEqual(8000); | ||
expect(backoff.fail()).toEqual(15000); | ||
}); | ||
|
||
it.each([10 * 1000, 60 * 1000])( | ||
'resets the delay when the last successful connection was connected greater than the retry reset interval', | ||
(retryResetInterval) => { | ||
let time = 1000; | ||
const backoff = new Backoff(1000, retryResetInterval, noJitter); | ||
expect(backoff.fail(time)).toEqual(1000); | ||
time += 1; | ||
backoff.success(time); | ||
time = time + retryResetInterval + 1; | ||
expect(backoff.fail(time)).toEqual(1000); | ||
time += 1; | ||
expect(backoff.fail(time)).toEqual(2000); | ||
time += 1; | ||
backoff.success(time); | ||
time = time + retryResetInterval + 1; | ||
expect(backoff.fail(time)).toEqual(1000); | ||
}, | ||
); | ||
|
||
it.each([10 * 1000, 60 * 1000])( | ||
'does not reset the delay when the connection did not persist longer than the retry reset interval', | ||
(retryResetInterval) => { | ||
const backoff = new Backoff(1000, retryResetInterval, noJitter); | ||
|
||
let time = 1000; | ||
expect(backoff.fail(time)).toEqual(1000); | ||
time += 1; | ||
backoff.success(time); | ||
time += retryResetInterval; | ||
expect(backoff.fail(time)).toEqual(2000); | ||
time += retryResetInterval; | ||
expect(backoff.fail(time)).toEqual(4000); | ||
time += 1; | ||
backoff.success(time); | ||
time += retryResetInterval; | ||
expect(backoff.fail(time)).toEqual(8000); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import common from '@rollup/plugin-commonjs'; | ||
import resolve from '@rollup/plugin-node-resolve'; | ||
import terser from '@rollup/plugin-terser'; | ||
import typescript from '@rollup/plugin-typescript'; | ||
import json from '@rollup/plugin-json'; | ||
|
||
const getSharedConfig = (format, file) => ({ | ||
input: 'src/index.ts', | ||
output: [ | ||
{ | ||
format: format, | ||
sourcemap: true, | ||
file: file, | ||
}, | ||
], | ||
onwarn: (warning) => { | ||
if (warning.code !== 'CIRCULAR_DEPENDENCY') { | ||
console.error(`(!) ${warning.message}`); | ||
} | ||
}, | ||
}); | ||
|
||
export default [ | ||
{ | ||
...getSharedConfig('es', 'dist/index.es.js'), | ||
plugins: [ | ||
typescript({ | ||
module: 'esnext', | ||
}), | ||
common({ | ||
transformMixedEsModules: true, | ||
esmExternals: true, | ||
}), | ||
resolve(), | ||
terser(), | ||
json(), | ||
], | ||
}, | ||
{ | ||
...getSharedConfig('cjs', 'dist/index.cjs.js'), | ||
plugins: [ | ||
typescript(), | ||
common(), | ||
resolve(), | ||
terser(), | ||
json(), | ||
], | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
const MAX_RETRY_DELAY = 30 * 1000; // Maximum retry delay 30 seconds. | ||
const JITTER_RATIO = 0.5; // Delay should be 50%-100% of calculated time. | ||
|
||
/** | ||
* Implements exponential backoff and jitter. This class tracks successful connections and failures | ||
* and produces a retry delay. | ||
* | ||
* It does not start any timers or directly control a connection. | ||
* | ||
* The backoff follows an exponential backoff scheme with 50% jitter starting at | ||
* initialRetryDelayMillis and capping at MAX_RETRY_DELAY. If RESET_INTERVAL has elapsed after a | ||
* success, without an intervening faulure, then the backoff is reset to initialRetryDelayMillis. | ||
*/ | ||
export default class Backoff { | ||
private retryCount: number = 0; | ||
private activeSince?: number; | ||
private initialRetryDelayMillis: number; | ||
/** | ||
* The exponent at which the backoff delay will exceed the maximum. | ||
* Beyond this limit the backoff can be set to the max. | ||
*/ | ||
private readonly maxExponent: number; | ||
|
||
constructor( | ||
initialRetryDelayMillis: number, | ||
private readonly retryResetIntervalMillis: number, | ||
private readonly random = Math.random, | ||
) { | ||
// Initial retry delay cannot be 0. | ||
this.initialRetryDelayMillis = Math.max(1, initialRetryDelayMillis); | ||
this.maxExponent = Math.ceil(Math.log2(MAX_RETRY_DELAY / this.initialRetryDelayMillis)); | ||
} | ||
|
||
private backoff(): number { | ||
const exponent = Math.min(this.retryCount, this.maxExponent); | ||
const delay = this.initialRetryDelayMillis * 2 ** exponent; | ||
return Math.min(delay, MAX_RETRY_DELAY); | ||
} | ||
|
||
private jitter(computedDelayMillis: number): number { | ||
return computedDelayMillis - Math.trunc(this.random() * JITTER_RATIO * computedDelayMillis); | ||
} | ||
|
||
/** | ||
* This function should be called when a connection attempt is successful. | ||
* | ||
* @param timeStampMs The time of the success. Used primarily for testing, when not provided | ||
* the current time is used. | ||
*/ | ||
success(timeStampMs: number = Date.now()): void { | ||
this.activeSince = timeStampMs; | ||
} | ||
|
||
/** | ||
* This function should be called when a connection fails. It returns the a delay, in | ||
* milliseconds, after which a reconnection attempt should be made. | ||
* | ||
* @param timeStampMs The time of the success. Used primarily for testing, when not provided | ||
* the current time is used. | ||
* @returns The delay before the next connection attempt. | ||
*/ | ||
fail(timeStampMs: number = Date.now()): number { | ||
// If the last successful connection was active for more than the RESET_INTERVAL, then we | ||
// return to the initial retry delay. | ||
if ( | ||
this.activeSince !== undefined && | ||
timeStampMs - this.activeSince > this.retryResetIntervalMillis | ||
) { | ||
this.retryCount = 0; | ||
} | ||
this.activeSince = undefined; | ||
const delay = this.jitter(this.backoff()); | ||
this.retryCount += 1; | ||
return delay; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { | ||
EventSourceCapabilities, | ||
EventSourceInitDict, | ||
EventSource as LDEventSource, | ||
Options, | ||
Requests, | ||
Response, | ||
} from '@launchdarkly/js-client-sdk-common'; | ||
|
||
import DefaultBrowserEventSource from './DefaultBrowserEventSource'; | ||
|
||
export default class BrowserRequests implements Requests { | ||
fetch(url: string, options?: Options): Promise<Response> { | ||
// @ts-ignore | ||
return fetch(url, options); | ||
} | ||
|
||
createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): LDEventSource { | ||
return new DefaultBrowserEventSource(url, eventSourceInitDict); | ||
} | ||
|
||
getEventSourceCapabilities(): EventSourceCapabilities { | ||
return { | ||
customMethod: false, | ||
readTimeout: false, | ||
headers: false, | ||
}; | ||
} | ||
} |
Oops, something went wrong.