Skip to content

Commit 061e920

Browse files
fix: apply client.registerAPIRequestMiddleware to SDK streaming requests (#163)
1 parent bf6cf7a commit 061e920

File tree

6 files changed

+63
-24
lines changed

6 files changed

+63
-24
lines changed

README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,21 @@ const client = initialize('00000000-1111-2222-3333-444444444444', {
8181
By default, Harness Feature Flags SDK has streaming enabled and polling enabled. Both modes can be toggled according to your preference using the SDK's configuration.
8282

8383
### Streaming Mode
84-
Streaming mode establishes a continuous connection between your application and the Feature Flags service.
85-
This allows for real-time updates on feature flags without requiring periodic checks.
84+
Streaming mode establishes a continuous connection between your application and the Feature Flags service.
85+
This allows for real-time updates on feature flags without requiring periodic checks.
8686
If an error occurs while streaming and `pollingEnabled` is set to `true`,
87-
the SDK will automatically fall back to polling mode until streaming can be reestablished.
87+
the SDK will automatically fall back to polling mode until streaming can be reestablished.
8888
If `pollingEnabled` is `false`, streaming will attempt to reconnect without falling back to polling.
8989

9090
### Polling Mode
9191
In polling mode, the SDK will periodically check with the Feature Flags service to retrieve updates for feature flags. The frequency of these checks can be adjusted using the SDK's configurations.
9292

9393
### No Streaming or Polling
94-
If both streaming and polling modes are disabled (`streamEnabled: false` and `pollingEnabled: false`),
95-
the SDK will not automatically fetch feature flag updates after the initial fetch.
94+
If both streaming and polling modes are disabled (`streamEnabled: false` and `pollingEnabled: false`),
95+
the SDK will not automatically fetch feature flag updates after the initial fetch.
9696
This means that after the initial load, any changes made to the feature flags on the Harness server will not be reflected in the application until the SDK is re-initialized or one of the modes is re-enabled.
9797

98-
This configuration might be useful in specific scenarios where you want to ensure a consistent set of feature flags
98+
This configuration might be useful in specific scenarios where you want to ensure a consistent set of feature flags
9999
for a session or when the application operates in an environment where regular updates are not necessary. However, it's essential to be aware that this configuration can lead to outdated flag evaluations if the flags change on the server.
100100

101101
To configure the modes:
@@ -127,9 +127,9 @@ You can configure the maximum number of streaming retries before the SDK stops a
127127
```typescript
128128
const options = {
129129
maxRetries: 5, // Set the maximum number of retries for streaming. Default is Infinity.
130-
streamEnabled: true,
131-
pollingEnabled: true,
132-
pollingInterval: 60000,
130+
streamEnabled: true,
131+
pollingEnabled: true,
132+
pollingInterval: 60000,
133133
}
134134

135135
const client = initialize(
@@ -173,7 +173,7 @@ client.on(Event.DISCONNECTED, () => {
173173
})
174174

175175
client.on(Event.CONNECTED, () => {
176-
// Event happens when connection has been lost and reestablished
176+
// Event happens when connection has been lost and reestablished
177177
})
178178

179179
client.on(Event.POLLING, () => {
@@ -238,7 +238,7 @@ For the example above, if the flag identifier 'Dark_Theme' is not found, result
238238
}
239239
```
240240

241-
If you do not need to know the default variation was returned:
241+
If you do not need to know the default variation was returned:
242242

243243
```typescript
244244
const variationValue = client.variation('Dark_Theme', false) // second argument is default value when variation does not exist
@@ -257,7 +257,7 @@ For the example above:
257257
3. Wrong project API key being used
258258

259259
#### Listening for the `ERROR_DEFAULT_VARIATION_RETURNED` event
260-
You can also listen for the `ERROR_DEFAULT_VARIATION_RETURNED` event, which is emitted whenever a default variation is returned because the flag has not been found in the cache. This is useful for logging or taking other action when a flag is not found.
260+
You can also listen for the `ERROR_DEFAULT_VARIATION_RETURNED` event, which is emitted whenever a default variation is returned because the flag has not been found in the cache. This is useful for logging or taking other action when a flag is not found.
261261

262262
Example of listening for the event:
263263

@@ -386,7 +386,7 @@ const client = initialize(
386386
```
387387

388388
## API Middleware
389-
The `registerAPIRequestMiddleware` function allows you to register a middleware function to manipulate the payload (URL, body and headers) of API requests after the AUTH call has successfully completed
389+
The `registerAPIRequestMiddleware` function allows you to register a middleware function to manipulate the payload (URL, body and headers) of API requests.
390390

391391
```typescript
392392
function abortControllerMiddleware([url, options]) {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@harnessio/ff-javascript-client-sdk",
3-
"version": "1.31.1",
3+
"version": "1.31.2",
44
"author": "Harness",
55
"license": "Apache-2.0",
66
"main": "dist/sdk.cjs.js",

src/__tests__/stream.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,29 @@ describe('Streamer', () => {
246246
expect(mockEventBus.emit).toHaveBeenCalledWith(Event.CONNECTED)
247247
expect(mockXHR.send).toHaveBeenCalledTimes(4) // Should attempt to reconnect 3 times before succeeding
248248
})
249+
250+
it('should apply middleware to requests', () => {
251+
const streamer = getStreamer()
252+
253+
const newHeader: string = 'header-value'
254+
255+
streamer.registerAPIRequestMiddleware(args => {
256+
args[0] = 'http://test/stream2'
257+
args[1].headers = { ...args[1].headers, newHeader }
258+
return args
259+
})
260+
261+
streamer.start()
262+
expect(mockXHR.open).toHaveBeenCalledWith('GET', 'http://test/stream2')
263+
expect(mockXHR.setRequestHeader).toHaveBeenCalledTimes(5)
264+
expect(mockXHR.setRequestHeader).toHaveBeenCalledWith('Test-Header', 'value')
265+
expect(mockXHR.setRequestHeader).toHaveBeenCalledWith('Cache-Control', 'no-cache')
266+
expect(mockXHR.setRequestHeader).toHaveBeenCalledWith('Accept', 'text/event-stream')
267+
expect(mockXHR.setRequestHeader).toHaveBeenCalledWith('API-Key', 'test-api-key')
268+
expect(mockXHR.setRequestHeader).toHaveBeenCalledWith('newHeader', 'header-value')
269+
expect(mockXHR.send).toHaveBeenCalled()
270+
271+
mockXHR.onprogress({} as ProgressEvent)
272+
expect(mockEventBus.emit).toHaveBeenCalledWith(Event.CONNECTED)
273+
})
249274
})

src/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ import { getVariation } from './variation'
2323
import Poller from './poller'
2424
import { createCacheIdSeed, getCache } from './cache'
2525

26-
const SDK_VERSION = '1.31.1'
26+
const SDK_VERSION = '1.31.2'
2727
const SDK_INFO = `Javascript ${SDK_VERSION} Client`
2828
const METRICS_VALID_COUNT_INTERVAL = 500
29-
const fetch = globalThis.fetch
3029

3130
// Flag to detect is Proxy is supported (not under IE 11)
3231
const hasProxy = !!globalThis.Proxy
@@ -35,12 +34,13 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
3534
let closed = false
3635
let environment: string
3736
let clusterIdentifier: string
38-
let eventSource: any
37+
let eventSource: Streamer | undefined
3938
let poller: Poller
4039
let jwtToken: string
4140
let metricsSchedulerId: number
4241
let standardHeaders: Record<string, string> = {}
43-
let fetchWithMiddleware = addMiddlewareToFetch(args => args)
42+
let defaultMiddleware: APIRequestMiddleware = args => args
43+
let fetchWithMiddleware = addMiddlewareToFetch(defaultMiddleware)
4444
let lastCacheRefreshTime = 0
4545
let initialised = false
4646
// We need to pause metrics in certain situations, such as when we are doing the initial evaluation load, and when
@@ -594,7 +594,8 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
594594
handleSegmentEvent(event)
595595
}
596596
},
597-
configurations.maxStreamRetries
597+
configurations.maxStreamRetries,
598+
defaultMiddleware
598599
)
599600
eventSource.start()
600601
}
@@ -678,7 +679,9 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
678679
}
679680

680681
const registerAPIRequestMiddleware = (middleware: APIRequestMiddleware): void => {
682+
defaultMiddleware = middleware
681683
fetchWithMiddleware = addMiddlewareToFetch(middleware)
684+
if (eventSource) eventSource.registerAPIRequestMiddleware(middleware)
682685
}
683686

684687
const refreshEvaluations = () => {

src/stream.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Event, type Options, StreamEvent } from './types'
1+
import { APIRequestMiddleware, Event, type Options, StreamEvent } from './types'
22
import { getRandom } from './utils'
33
import type Poller from './poller'
44
import type { Emitter } from 'mitt'
@@ -24,9 +24,14 @@ export class Streamer {
2424
private logDebug: (...data: any[]) => void,
2525
private logError: (...data: any[]) => void,
2626
private eventCallback: (e: StreamEvent) => void,
27-
private maxRetries: number
27+
private maxRetries: number,
28+
private middleware?: APIRequestMiddleware
2829
) {}
2930

31+
registerAPIRequestMiddleware(middleware: APIRequestMiddleware): void {
32+
this.middleware = middleware
33+
};
34+
3035
start() {
3136
const processData = (data: any): void => {
3237
data.toString().split(/\r?\n/).forEach(processLine)
@@ -94,13 +99,19 @@ export class Streamer {
9499
onDisconnect()
95100
}
96101

97-
const sseHeaders: Record<string, string> = {
102+
let sseHeaders: Record<string, string> = {
98103
'Cache-Control': 'no-cache',
99104
Accept: 'text/event-stream',
100105
'API-Key': this.apiKey,
101106
...this.standardHeaders
102107
}
103108

109+
if (this.middleware) {
110+
const [url, options] = this.middleware([this.url, { headers: sseHeaders }])
111+
this.url = url as string
112+
sseHeaders = options?.headers as Record<string, string> || {}
113+
}
114+
104115
this.logDebugMessage('SSE HTTP start request', this.url)
105116

106117
this.xhr = new XMLHttpRequest()

0 commit comments

Comments
 (0)