1- import type { ClientReadableStream , ClientUnaryCall , ServiceError } from '@grpc/grpc-js' ;
1+ import { type ClientReadableStream , type ClientUnaryCall , type ServiceError } from '@grpc/grpc-js' ;
22import { status } from '@grpc/grpc-js' ;
33import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state' ;
44import type { EvaluationContext , FlagValue , JsonValue , Logger , ResolutionDetails } from '@openfeature/server-sdk' ;
@@ -27,7 +27,12 @@ import type {
2727} from '../../../proto/ts/flagd/evaluation/v1/evaluation' ;
2828import { ServiceClient } from '../../../proto/ts/flagd/evaluation/v1/evaluation' ;
2929import type { FlagdGrpcConfig } from '../../configuration' ;
30- import { DEFAULT_MAX_CACHE_SIZE , EVENT_CONFIGURATION_CHANGE , EVENT_PROVIDER_READY } from '../../constants' ;
30+ import {
31+ DEFAULT_MAX_BACKOFF_MS ,
32+ DEFAULT_MAX_CACHE_SIZE ,
33+ EVENT_CONFIGURATION_CHANGE ,
34+ EVENT_PROVIDER_READY ,
35+ } from '../../constants' ;
3136import { FlagdProvider } from '../../flagd-provider' ;
3237import type { Service } from '../service' ;
3338import {
@@ -79,6 +84,8 @@ export class GRPCService implements Service {
7984 private readonly _fatalStatusCodes : Set < number > ;
8085 private _initialized = false ;
8186 private _streamDeadline : number ;
87+ private _maxBackoffMs : number ;
88+ private _errorThrottled = false ;
8289
8390 private get _cacheActive ( ) {
8491 // the cache is "active" (able to be used) if the config enabled it, AND the gRPC stream is live
@@ -94,6 +101,7 @@ export class GRPCService implements Service {
94101 const clientOptions = buildClientOptions ( config ) ;
95102 const channelCredentials = createChannelCredentials ( tls , certPath ) ;
96103
104+ this . _maxBackoffMs = config . retryBackoffMaxMs || DEFAULT_MAX_BACKOFF_MS ;
97105 this . _client = client
98106 ? client
99107 : new ServiceClient ( socketPath ? `unix://${ socketPath } ` : `${ host } :${ port } ` , channelCredentials , clientOptions ) ;
@@ -175,32 +183,48 @@ export class GRPCService implements Service {
175183 // close the previous stream if we're reconnecting
176184 closeStreamIfDefined ( this . _eventStream ) ;
177185
178- const deadline = this . _streamDeadline != 0 ? Date . now ( ) + this . _streamDeadline : undefined ;
179- const stream = this . _client . eventStream ( { waitForReady : true } , { deadline } ) ;
180- stream . on ( 'error' , ( err : Error ) => {
181- // Check if error is a fatal status code on first connection only
182- if ( isFatalStatusCodeError ( err , this . _initialized , this . _fatalStatusCodes ) ) {
183- handleFatalStatusCodeError ( err , this . logger , disconnectCallback , rejectConnect ) ;
184- return ;
185- }
186- rejectConnect ?.( err ) ;
187- this . handleError ( reconnectCallback , changedCallback , disconnectCallback ) ;
188- } ) ;
189- stream . on ( 'data' , ( message ) => {
190- if ( message . type === EVENT_PROVIDER_READY ) {
191- this . logger ?. debug ( `${ FlagdProvider . name } : streaming connection established with flagd` ) ;
192- this . _initialized = true ;
193- // if resolveConnect is undefined, this is a reconnection; we only want to fire the reconnect callback in that case
194- if ( resolveConnect ) {
195- resolveConnect ( ) ;
196- } else {
197- reconnectCallback ( ) ;
186+ // wait for connection to be stable
187+ this . _client . waitForReady ( Date . now ( ) + this . _deadline , ( err ) => {
188+ if ( err ) {
189+ // Check if error is a fatal status code on first connection only
190+ if ( isFatalStatusCodeError ( err , this . _initialized , this . _fatalStatusCodes ) ) {
191+ handleFatalStatusCodeError ( err , this . logger , disconnectCallback , rejectConnect ) ;
192+ return ;
198193 }
199- } else if ( message . type === EVENT_CONFIGURATION_CHANGE ) {
200- this . handleFlagsChanged ( message , changedCallback ) ;
194+ rejectConnect ?.( err ) ;
195+ this . handleError ( reconnectCallback , changedCallback , disconnectCallback ) ;
196+ } else {
197+ const streamDeadline = this . _streamDeadline != 0 ? Date . now ( ) + this . _streamDeadline : undefined ;
198+ const stream = this . _client . eventStream ( { } , { deadline : streamDeadline } ) ;
199+ stream . on ( 'error' , ( err : Error ) => {
200+ // In cases where we get an explicit error status, we add a delay.
201+ // This prevents tight loops when errors are returned immediately, typically by intervening proxies like Envoy.
202+ this . _errorThrottled = true ;
203+ // Check if error is a fatal status code on first connection only
204+ if ( isFatalStatusCodeError ( err , this . _initialized , this . _fatalStatusCodes ) ) {
205+ handleFatalStatusCodeError ( err , this . logger , disconnectCallback , rejectConnect ) ;
206+ return ;
207+ }
208+ rejectConnect ?.( err ) ;
209+ this . handleError ( reconnectCallback , changedCallback , disconnectCallback ) ;
210+ } ) ;
211+ stream . on ( 'data' , ( message ) => {
212+ if ( message . type === EVENT_PROVIDER_READY ) {
213+ this . logger ?. debug ( `${ FlagdProvider . name } : streaming connection established with flagd` ) ;
214+ this . _initialized = true ;
215+ // if resolveConnect is undefined, this is a reconnection; we only want to fire the reconnect callback in that case
216+ if ( resolveConnect ) {
217+ resolveConnect ( ) ;
218+ } else {
219+ reconnectCallback ( ) ;
220+ }
221+ } else if ( message . type === EVENT_CONFIGURATION_CHANGE ) {
222+ this . handleFlagsChanged ( message , changedCallback ) ;
223+ }
224+ } ) ;
225+ this . _eventStream = stream ;
201226 }
202227 } ) ;
203- this . _eventStream = stream ;
204228 }
205229
206230 private handleFlagsChanged ( message : EventStreamResponse , changedCallback : ( flagsChanged : string [ ] ) => void ) {
@@ -228,10 +252,11 @@ export class GRPCService implements Service {
228252 changedCallback : ( flagsChanged : string [ ] ) => void ,
229253 disconnectCallback : ( message : string ) => void ,
230254 ) {
231- const channel = this . _client . getChannel ( ) ;
232- channel . watchConnectivityState ( channel . getConnectivityState ( true ) , Infinity , ( ) => {
233- this . listen ( reconnectCallback , changedCallback , disconnectCallback ) ;
234- } ) ;
255+ setTimeout (
256+ ( ) => this . listen ( reconnectCallback , changedCallback , disconnectCallback ) ,
257+ this . _errorThrottled ? this . _maxBackoffMs : 0 ,
258+ ) ;
259+ this . _errorThrottled = false ;
235260 }
236261
237262 private handleError (
0 commit comments