Skip to content

Commit cbc76ed

Browse files
authored
fix: add shutdown to flagd-web provider (#325)
Signed-off-by: Todd Baert <toddbaert@gmail.com>
1 parent 10a3239 commit cbc76ed

File tree

2 files changed

+48
-12
lines changed

2 files changed

+48
-12
lines changed

libs/providers/flagd-web/src/lib/flagd-web-provider.spec.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CallbackClient, Code, ConnectError, PromiseClient } from '@bufbuild/connect-web';
1+
import { CallbackClient, Code, ConnectError, PromiseClient } from '@bufbuild/connect';
22
import { Struct } from '@bufbuild/protobuf';
33
import { Client, ErrorCode, JsonValue, OpenFeature, ProviderEvents, StandardResolutionReasons } from '@openfeature/web-sdk';
44
import fetchMock from 'jest-fetch-mock';
@@ -35,14 +35,19 @@ class MockCallbackClient implements Partial<CallbackClient<typeof Service>> {
3535
*/
3636
fail = false;
3737

38+
// use to fire an incoming mock message
3839
mockMessage(message: Partial<EventStreamResponse>) {
3940
this.messageCallback?.(message as EventStreamResponse);
4041
}
4142

43+
// use to fire a mock connection close
4244
mockClose(error: Partial<ConnectError>) {
4345
this.closeCallback?.(error as ConnectError);
4446
}
4547

48+
// cancel function stub to make assertions against
49+
cancelFunction = jest.fn(() => undefined);
50+
4651
eventStream = jest.fn(
4752
(
4853
_,
@@ -57,7 +62,7 @@ class MockCallbackClient implements Partial<CallbackClient<typeof Service>> {
5762
setTimeout(() => this.closeCallback?.({ code: Code.Unavailable } as unknown as ConnectError), 0);
5863
}
5964

60-
return () => undefined;
65+
return this.cancelFunction;
6166
}
6267
);
6368
}
@@ -239,6 +244,34 @@ describe(FlagdWebProvider.name, () => {
239244
});
240245
});
241246

247+
describe('shutdown', () => {
248+
let client: Client;
249+
let mockCallbackClient: MockCallbackClient;
250+
const mockPromiseClient = new MockPromiseClient() as unknown as PromiseClient<typeof Service>;
251+
const context = { some: 'value' };
252+
253+
beforeEach(() => {
254+
mockCallbackClient = new MockCallbackClient();
255+
OpenFeature.setProvider(
256+
new FlagdWebProvider(
257+
{ host: 'fake.com', maxRetries: -1 },
258+
console,
259+
mockPromiseClient,
260+
mockCallbackClient as unknown as CallbackClient<typeof Service>
261+
)
262+
);
263+
client = OpenFeature.getClient('events-test');
264+
});
265+
266+
describe('API close', () => {
267+
it('should call cancel function on provider', () => {
268+
expect(mockCallbackClient.cancelFunction).not.toHaveBeenCalled();
269+
OpenFeature.close();
270+
expect(mockCallbackClient.cancelFunction).toHaveBeenCalled();
271+
});
272+
});
273+
});
274+
242275
describe('reconnect logic', () => {
243276
describe('Infinite maxRetries', () => {
244277
it('should attempt reconnect many times', (done) => {

libs/providers/flagd-web/src/lib/flagd-web-provider.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import {
2-
CallbackClient,
3-
createCallbackClient,
4-
createConnectTransport,
5-
createPromiseClient,
6-
PromiseClient,
7-
} from '@bufbuild/connect-web';
1+
import { CallbackClient, createCallbackClient, createPromiseClient, PromiseClient } from '@bufbuild/connect';
2+
import { createConnectTransport } from '@bufbuild/connect-web';
83
import { Struct } from '@bufbuild/protobuf';
94
import {
105
EvaluationContext,
@@ -47,6 +42,7 @@ export class FlagdWebProvider implements Provider {
4742
private _delayMs = INITIAL_DELAY_MS;
4843
private _logger?: Logger;
4944
private _flags: { [key: string]: ResolutionDetails<FlagValue> & { type: AnyFlagResolutionType } } = {};
45+
private _cancelFunction: (() => void) | undefined;
5046

5147
constructor(
5248
options: FlagdProviderOptions,
@@ -91,6 +87,11 @@ export class FlagdWebProvider implements Provider {
9187
return this.evaluate(flagKey, 'objectValue');
9288
}
9389

90+
onClose(): Promise<void> {
91+
// close the stream using the saved cancel function
92+
return Promise.resolve(this._cancelFunction?.());
93+
}
94+
9495
private evaluate<T extends FlagValue>(flagKey: string, type: AnyFlagResolutionType): ResolutionDetails<T> {
9596
const resolved = this._flags[flagKey];
9697
if (!resolved) {
@@ -100,6 +101,7 @@ export class FlagdWebProvider implements Provider {
100101
throw new TypeMismatchError(`flag key ${flagKey} is not of type ${type}`);
101102
}
102103
return {
104+
// return reason=CACHED if we're disconnected since we can't guarantee things are up to date
103105
reason: this._connected ? resolved.reason : StandardResolutionReasons.CACHED,
104106
variant: resolved.variant,
105107
value: resolved.value as T,
@@ -110,7 +112,7 @@ export class FlagdWebProvider implements Provider {
110112
this._delayMs = Math.min(this._delayMs * BACK_OFF_MULTIPLIER, this._maxDelay);
111113

112114
return new Promise<void>((resolve) => {
113-
this._callbackClient.eventStream(
115+
this._cancelFunction = this._callbackClient.eventStream(
114116
{},
115117
(message) => {
116118
// get the context at the time of the message
@@ -131,8 +133,9 @@ export class FlagdWebProvider implements Provider {
131133
}
132134
}
133135
},
134-
() => {
135-
this._logger?.error(`${FlagdWebProvider.name}: could not establish connection to flagd`);
136+
(err) => {
137+
this._logger?.error(`${FlagdWebProvider.name}: could not establish connection to flagd, ${err?.message}`);
138+
this._logger?.debug(err?.stack);
136139
if (this._retry < this._maxRetries) {
137140
this._retry++;
138141
setTimeout(() => this.retryConnect(), this._delayMs);

0 commit comments

Comments
 (0)