Skip to content

Commit

Permalink
feat: [FFM-6489]: Allow side-loading of evaluations (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
knagurski authored Jan 24, 2023
1 parent 6f98f3b commit c2b43b6
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 89 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,22 +120,20 @@ In case you want to import this library directly (without having to use npm or y

```html
<script type="module">
import { initialize, Event } from 'https://unpkg.com/@harnessio/ff-javascript-client-sdk@1.7.0/dist/sdk.client.js'
import { initialize, Event } from 'https://unpkg.com/@harnessio/ff-javascript-client-sdk/dist/sdk.client.js'
</script>
```

If you need to support old browsers which don't support ES Module:

```html
<script src="https://unpkg.com/@harnessio/ff-javascript-client-sdk@1.7.0/dist/sdk.client.js"></script>
<script src="https://unpkg.com/@harnessio/ff-javascript-client-sdk/dist/sdk.client.js"></script>
<script>
var initialize = HarnessFFSDK.initialize
var Event = HarnessFFSDK.Event
</script>
```

Remember to change the version `1.7.0` in the unpkg url accordingly.

## License

Apache version 2.
12 changes: 6 additions & 6 deletions dist/sdk.cjs.js

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions dist/sdk.client-iife.js

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions dist/sdk.client.js

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions dist/sdk.esm.js

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion dist/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ export declare enum Event {
CONNECTED = "connected",
DISCONNECTED = "disconnected",
CHANGED = "changed",
ERROR = "error"
ERROR = "error",
ERROR_METRICS = "metrics error",
ERROR_AUTH = "auth error",
ERROR_FETCH_FLAGS = "fetch flags error",
ERROR_FETCH_FLAG = "fetch flag error",
ERROR_STREAM = "stream error"
}
export declare type VariationValue = boolean | string | number | object | undefined;
export interface Evaluation {
Expand All @@ -31,6 +36,11 @@ export interface EventCallbackMapping {
[Event.DISCONNECTED]: () => void;
[Event.CHANGED]: (flag: Evaluation) => void;
[Event.ERROR]: (error: unknown) => void;
[Event.ERROR_AUTH]: (error: unknown) => void;
[Event.ERROR_FETCH_FLAGS]: (error: unknown) => void;
[Event.ERROR_FETCH_FLAG]: (error: unknown) => void;
[Event.ERROR_STREAM]: (error: unknown) => void;
[Event.ERROR_METRICS]: (error: unknown) => void;
}
export declare type EventOnBinding = <K extends keyof EventCallbackMapping>(event: K, callback: EventCallbackMapping[K]) => void;
export declare type EventOffBinding = <K extends keyof EventCallbackMapping>(event?: K, callback?: EventCallbackMapping[K]) => void;
Expand All @@ -39,6 +49,7 @@ export interface Result {
off: EventOffBinding;
variation: (identifier: string, defaultValue: any) => VariationValue;
close: () => void;
setEvaluations: (evaluations: Evaluation[]) => void;
}
export interface Options {
baseUrl?: string;
Expand Down
2 changes: 1 addition & 1 deletion examples/preact/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/react-redux/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@harnessio/ff-javascript-client-sdk",
"version": "1.7.0",
"version": "1.8.0",
"author": "Harness",
"license": "Apache-2.0",
"main": "dist/sdk.cjs.js",
Expand All @@ -16,6 +16,7 @@
"build:esm": "esbuild ./src/index.ts --minify --bundle --target=es2016 --format=esm --external:jwt-decode --external:mitt --external:event-source-polyfill --outfile=./dist/sdk.esm.js",
"build:cjs": "esbuild ./src/index.ts --minify --bundle --target=es2016 --platform=node --format=cjs --external:jwt-decode --external:mitt --external:event-source-polyfill --outfile=./dist/sdk.cjs.js",
"build": "npm run clean; npm run type; npm run build:esm; npm run build:cjs; npm run build:client; npm run build:client-esm",
"install-examples": "cd examples/preact; rm -rf node_modules; npm i; cd -; cd examples/react; rm -rf node_modules; npm i; cd -; cd examples/react-redux; rm -rf node_modules; npm i; cd -",
"format": "prettier --write \"src/**/*.ts\"",
"lint": "tslint -p tsconfig.json",
"type": "tsc ./src/*.ts --declaration --emitDeclarationOnly --outDir dist --lib ES2015,DOM",
Expand Down
106 changes: 53 additions & 53 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
import { Event } from './types'
import { defaultOptions, logError, MIN_EVENTS_SYNC_INTERVAL } from './utils'

const SDK_VERSION = '1.7.0'
const SDK_VERSION = '1.8.0'
const METRICS_VALID_COUNT_INTERVAL = 500
const fetch = globalThis.fetch
const EventSource = EventSourcePolyfill
Expand Down Expand Up @@ -172,6 +172,7 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
failedMetricsCallCount = 0
}
logDebug(error)
eventBus.emit(Event.ERROR_METRICS, error)
})
.finally(() => {
metricsSchedulerId = window.setTimeout(scheduleSendingMetrics, configurations.eventsSyncInterval)
Expand Down Expand Up @@ -307,6 +308,8 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
environment = decoded.environment
clusterIdentifier = decoded.clusterIdentifier

const hasExistingFlags = !!Object.keys(evaluations).length

// When authentication is done, fetch all flags
fetchFlags()
.then(() => {
Expand All @@ -321,18 +324,10 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
if (closed) return

logDebug('Event stream ready', { storage })
eventBus.emit(Event.READY, { ...storage })

if (!hasProxy) {
Object.keys(storage).forEach(key => {
metrics.push({
featureIdentifier: key,
featureValue: storage[key],
variationIdentifier: evaluations[key]?.identifier || '',
count: metricsCollectorEnabled ? 1 : 0,
lastAccessed: Date.now()
})
})
// emit the ready event only if flags weren't already set using setEvaluations
if (!hasExistingFlags) {
eventBus.emit(Event.READY, { ...storage })
}
})
.catch(err => {
Expand All @@ -341,6 +336,7 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
})
.catch(error => {
logError('Authentication error: ', error)
eventBus.emit(Event.ERROR_AUTH, error)
eventBus.emit(Event.ERROR, error)
})

Expand All @@ -352,22 +348,18 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
headers: standardHeaders
}
)
const data = await res.json()

data.forEach((_evaluation: Evaluation) => {
const _value = convertValue(_evaluation)

// Update the flag if the values are different
const _oldValue = storage[_evaluation.flag]
if (_value !== _oldValue) {
logDebug('Flag variation has changed for ', _evaluation.identifier)
storage[_evaluation.flag] = _value
evaluations[_evaluation.flag] = { ..._evaluation, value: _value }
sendEvent(_evaluation)
}
})

if (res.ok) {
const data = await res.json()
data.forEach(registerEvaluation)
} else {
logError('Features fetch operation error: ', res)
eventBus.emit(Event.ERROR_FETCH_FLAGS, res)
eventBus.emit(Event.ERROR, res)
}
} catch (error) {
logError('Features fetch operation error: ', error)
eventBus.emit(Event.ERROR_FETCH_FLAGS, error)
eventBus.emit(Event.ERROR, error)
return error
}
Expand All @@ -384,43 +376,35 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =

if (result.ok) {
const flagInfo: Evaluation = await result.json()
const _value = convertValue(flagInfo)

stopMetricsCollector()
storage[identifier] = _value
evaluations[identifier] = { ...flagInfo, value: _value }
startMetricsCollector()

registerEvaluation(flagInfo)
sendEvent(flagInfo)

if (!hasProxy) {
const featureIdentifier = flagInfo.flag
const entry = metrics.find(
_entry => _entry.featureIdentifier === featureIdentifier && _entry.featureValue === flagInfo.value
)

if (entry) {
updateMetrics(entry)
entry.variationIdentifier = evaluations[featureIdentifier as string]?.identifier || ''
} else {
metrics.push({
featureIdentifier: featureIdentifier as string,
featureValue: String(flagInfo.value),
variationIdentifier: evaluations[featureIdentifier].identifier || '',
count: metricsCollectorEnabled ? 1 : 0,
lastAccessed: Date.now()
})
}
}
} else {
logError('Feature fetch operation error: ', result)
eventBus.emit(Event.ERROR_FETCH_FLAG, result)
eventBus.emit(Event.ERROR, result)
}
} catch (error) {
logError('Feature fetch operation error: ', error)
eventBus.emit(Event.ERROR_FETCH_FLAG, error)
eventBus.emit(Event.ERROR, error)
}
}

const registerEvaluation = (evaluation: Evaluation) => {
stopMetricsCollector()
const value = convertValue(evaluation)

// Update the flag if the values are different
if (value !== storage[evaluation.flag]) {
logDebug('Flag variation has changed for ', evaluation.identifier)
storage[evaluation.flag] = value
evaluations[evaluation.flag] = { ...evaluation, value: value }
sendEvent(evaluation)
}
startMetricsCollector()
}

const startStream = () => {
// TODO: Implement polling when stream is disabled
if (!configurations.streamEnabled) {
Expand All @@ -446,6 +430,7 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =

eventSource.onerror = (event: any) => {
logError('Stream has issue', event)
eventBus.emit(Event.ERROR_STREAM, event)
eventBus.emit(Event.ERROR, event)
}

Expand Down Expand Up @@ -537,7 +522,22 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
}
}

return { on, off, variation, close }
const setEvaluations = (evals: Evaluation[]): void => {
if (evals.length) {
const hasExistingFlags = !!Object.keys(evaluations).length

evals.forEach(registerEvaluation)

if (!hasExistingFlags) {
// defer for 10ms to allow ready handlers to be registered
setTimeout(() => {
eventBus.emit(Event.READY, { ...storage })
}, 10)
}
}
}

return { on, off, variation, close, setEvaluations }
}

export {
Expand Down
13 changes: 12 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ export enum Event {
CONNECTED = 'connected',
DISCONNECTED = 'disconnected',
CHANGED = 'changed',
ERROR = 'error'
ERROR = 'error',
ERROR_METRICS = 'metrics error',
ERROR_AUTH = 'auth error',
ERROR_FETCH_FLAGS = 'fetch flags error',
ERROR_FETCH_FLAG = 'fetch flag error',
ERROR_STREAM = 'stream error'
}

export type VariationValue = boolean | string | number | object | undefined
Expand All @@ -36,6 +41,11 @@ export interface EventCallbackMapping {
[Event.DISCONNECTED]: () => void
[Event.CHANGED]: (flag: Evaluation) => void
[Event.ERROR]: (error: unknown) => void
[Event.ERROR_AUTH]: (error: unknown) => void
[Event.ERROR_FETCH_FLAGS]: (error: unknown) => void
[Event.ERROR_FETCH_FLAG]: (error: unknown) => void
[Event.ERROR_STREAM]: (error: unknown) => void
[Event.ERROR_METRICS]: (error: unknown) => void
}

export type EventOnBinding = <K extends keyof EventCallbackMapping>(event: K, callback: EventCallbackMapping[K]) => void
Expand All @@ -49,6 +59,7 @@ export interface Result {
off: EventOffBinding
variation: (identifier: string, defaultValue: any) => VariationValue
close: () => void
setEvaluations: (evaluations: Evaluation[]) => void
}

export interface Options {
Expand Down

0 comments on commit c2b43b6

Please sign in to comment.