Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,14 @@ interface CacheOptions {
// storage mechanism to use, conforming to the Web Storage API standard, can be either synchronous or asynchronous
// defaults to localStorage
storage?: AsyncStorage | SyncStorage
/**
* use target attributes when deriving the cache key
* when set to `false` or omitted, the key will be formed using only the target identifier and SDK key
* when set to `true`, all target attributes with be used in addition to the target identifier and SDK key
* can be set to an array of target attributes to use a subset in addition to the target identifier and SDK key
* defaults to false
*/
deriveKeyFromTargetAttributes?: boolean | string[]
}
```

Expand Down Expand Up @@ -355,7 +363,7 @@ If the request is aborted due to this timeout the SDK will fail to initialize an

The default value if not specified is `0` which means that no timeout will occur.

**This only applies to the authentiaction request. If you wish to set a read timeout on the remaining requests made by the SDK, you may register [API Middleware](#api-middleware)
**This only applies to the authentication request. If you wish to set a read timeout on the remaining requests made by the SDK, you may register [API Middleware](#api-middleware)

```typescript
const options = {
Expand Down
17 changes: 9 additions & 8 deletions 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 package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@harnessio/ff-javascript-client-sdk",
"version": "1.29.0",
"version": "1.30.0",
"author": "Harness",
"license": "Apache-2.0",
"main": "dist/sdk.cjs.js",
Expand Down
39 changes: 37 additions & 2 deletions src/__tests__/cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AsyncStorage, Evaluation, SyncStorage } from '../types'
import { getCache } from '../cache'
import type { AsyncStorage, Evaluation, SyncStorage, Target } from '../types'
import { createCacheIdSeed, getCache } from '../cache'

const sampleEvaluations: Evaluation[] = [
{ flag: 'flag1', value: 'false', kind: 'boolean', identifier: 'false' },
Expand Down Expand Up @@ -149,3 +149,38 @@ describe('getCache', () => {
})
})
})

describe('createCacheIdSeed', () => {
const apiKey = 'abc123'
const target: Target = {
name: 'Test Name',
identifier: 'test-identifier',
attributes: {
a: 'bcd',
b: 123,
c: ['x', 'y', 'z']
}
}

test('it should return the target id and api key when deriveKeyFromTargetAttributes is omitted', async () => {
expect(createCacheIdSeed(target, apiKey)).toEqual(target.identifier + apiKey)
})

test('it should return the target id and api key when deriveKeyFromTargetAttributes is false', async () => {
expect(createCacheIdSeed(target, apiKey, { deriveKeyFromTargetAttributes: false })).toEqual(
target.identifier + apiKey
)
})

test('it should return the target id and api key with all attributes when deriveKeyFromTargetAttributes is true', async () => {
expect(createCacheIdSeed(target, apiKey, { deriveKeyFromTargetAttributes: true })).toEqual(
'{"a":"bcd","b":123,"c":["x","y","z"]}test-identifierabc123'
)
})

test('it should return the target id and api key with a subset of attributes when deriveKeyFromTargetAttributes is an array', async () => {
expect(createCacheIdSeed(target, apiKey, { deriveKeyFromTargetAttributes: ['a', 'c'] })).toEqual(
'{"a":"bcd","c":["x","y","z"]}test-identifierabc123'
)
})
})
27 changes: 25 additions & 2 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AsyncStorage, CacheOptions, Evaluation, SyncStorage } from './types'
import type { AsyncStorage, CacheOptions, Evaluation, SyncStorage, Target } from './types'
import { sortEvaluations } from './utils'

export interface GetCacheResponse {
loadFromCache: () => Promise<Evaluation[]>
Expand Down Expand Up @@ -48,7 +49,7 @@ async function clearCachedEvaluations(cacheId: string, storage: AsyncStorage): P
}

async function saveToCache(cacheId: string, storage: AsyncStorage, evaluations: Evaluation[]): Promise<void> {
await storage.setItem(cacheId, JSON.stringify(evaluations))
await storage.setItem(cacheId, JSON.stringify(sortEvaluations(evaluations)))
await storage.setItem(cacheId + '.ts', Date.now().toString())
}

Expand Down Expand Up @@ -76,6 +77,28 @@ async function removeCachedEvaluation(cacheId: string, storage: AsyncStorage, fl
}
}

export function createCacheIdSeed(target: Target, apiKey: string, config: CacheOptions = {}) {
if (!config.deriveKeyFromTargetAttributes) return target.identifier + apiKey

return (
JSON.stringify(
Object.keys(target.attributes || {})
.sort()
.filter(
attribute =>
!Array.isArray(config.deriveKeyFromTargetAttributes) ||
config.deriveKeyFromTargetAttributes.includes(attribute)
)
.reduce(
(filteredAttributes, attribute) => ({ ...filteredAttributes, [attribute]: target.attributes[attribute] }),
{}
)
) +
target.identifier +
apiKey
)
}

async function getCacheId(seed: string): Promise<string> {
let cacheId = seed

Expand Down
13 changes: 6 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import type {
DefaultVariationEventPayload
} from './types'
import { Event } from './types'
import { defer, encodeTarget, getConfiguration } from './utils'
import { defer, encodeTarget, getConfiguration, sortEvaluations } from './utils'
import { addMiddlewareToFetch } from './request'
import { Streamer } from './stream'
import { getVariation } from './variation'
import Poller from './poller'
import { getCache } from './cache'
import { createCacheIdSeed, getCache } from './cache'

const SDK_VERSION = '1.26.1'
const SDK_INFO = `Javascript ${SDK_VERSION} Client`
Expand Down Expand Up @@ -110,10 +110,9 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
try {
let initialLoad = true

const cache = await getCache(
target.identifier + apiKey,
typeof configurations.cache === 'boolean' ? {} : configurations.cache
)
const cacheConfig = typeof configurations.cache === 'boolean' ? {} : configurations.cache

const cache = await getCache(createCacheIdSeed(target, apiKey, cacheConfig), cacheConfig)

const cachedEvaluations = await cache.loadFromCache()

Expand Down Expand Up @@ -441,7 +440,7 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
)

if (res.ok) {
const data = await res.json()
const data = sortEvaluations(await res.json())
data.forEach(registerEvaluation)
eventBus.emit(Event.FLAGS_LOADED, data)
return { type: 'success', data: data }
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ export interface CacheOptions {
* @default localStorage
*/
storage?: AsyncStorage | SyncStorage
/**
* Use target attributes when deriving the cache key
* When set to `false` or omitted, the key will be formed using only the target identifier and SDK key
* When set to `true`, all target attributes with be used in addition to the target identifier and SDK key
* Can be set to an array of target attributes to use a subset in addition to the target identifier and SDK key
* @default false
*/
deriveKeyFromTargetAttributes?: boolean | string[]
}

export interface Logger {
Expand Down
6 changes: 5 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Options, Target } from './types'
import type { Evaluation, Options, Target } from './types'

export const MIN_EVENTS_SYNC_INTERVAL = 60000
export const MIN_POLLING_INTERVAL = 60000
Expand Down Expand Up @@ -99,3 +99,7 @@ const utf8encode = (str: string): string =>
)
})
.join('')

export function sortEvaluations(evaluations: Evaluation[]): Evaluation[] {
return [...evaluations].sort(({ flag: flagA }, { flag: flagB }) => (flagA < flagB ? -1 : 1))
}