Skip to content

Commit 387ea6b

Browse files
committed
Add fallback request
1 parent 984c318 commit 387ea6b

6 files changed

Lines changed: 139 additions & 0 deletions

File tree

docs/reference-tables/ea-settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
| TLS_PASSPHRASE | string | | Password to be used to generate an encryption key | | |
6262
| TLS_PRIVATE_KEY | string | undefined | Base64 Private Key of TSL/SSL certificate | - Value must be a valid base64 string | |
6363
| TLS_PUBLIC_KEY | string | undefined | Base64 Public Key of TSL/SSL certificate | - Value must be a valid base64 string | |
64+
| TRANSPORT_FALLBACK_ENABLED | boolean | false | Flag to enable endpoint fallback transports when configured | | |
6465
| WARMUP_SUBSCRIPTION_TTL | number | 300000 | TTL for batch warmer subscriptions | - Value must be an integer<br> - Value must be above the minimum<br> - Value must be below the maximum | 0 | 3600000 |
6566
| WS_CONNECTION_OPEN_TIMEOUT | number | 10000 | The maximum amount of time in milliseconds to wait for the websocket connection to open (including custom open handler) | - Value must be an integer<br> - Value must be above the minimum<br> - Value must be below the maximum | 500 | 30000 |
6667
| WS_HEARTBEAT_INTERVAL_MS | number | 10000 | The number of ms between each hearbeat message that EA sends to server, only works if heartbeat handler is provided | - Value must be an integer<br> - Value must be above the minimum<br> - Value must be below the maximum | 5000 | 300000 |

src/adapter/basic.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
highestRateLimitTiers,
1616
} from '../rate-limiting'
1717
import { RateLimiterFactory, RateLimitingStrategy } from '../rate-limiting/factory'
18+
import { Transport } from '../transports'
1819
import {
1920
AdapterRequest,
2021
AdapterResponse,
@@ -449,6 +450,21 @@ export class Adapter<
449450
const endpoint = this.endpointsMap[req.requestContext.endpointName]
450451
const transport = endpoint.transportRoutes.get(req.requestContext.transportName)
451452

453+
// TODO: Handle fallback transport
454+
const fallbackTransportName = endpoint.getFallbackTransportNameForRequest(
455+
req.requestContext.transportName,
456+
this.config.settings,
457+
)
458+
this.createFallbackRequest(req, fallbackTransportName)
459+
460+
return this.handleSingleTransportRequest(req, replySent, transport)
461+
}
462+
463+
private async handleSingleTransportRequest(
464+
req: AdapterRequest<EmptyInputParameters>,
465+
replySent: Promise<unknown>,
466+
transport: Transport<EndpointGenerics>,
467+
): Promise<Readonly<AdapterResponse>> {
452468
// First try to find the response in our cache, keep it ready
453469
const cachedResponse = await this.findResponseInCache(req)
454470

@@ -544,4 +560,19 @@ export class Adapter<
544560
statusCode: 504,
545561
})
546562
}
563+
564+
private createFallbackRequest(req: AdapterRequest<EmptyInputParameters>, transportName?: string) {
565+
if (!transportName) {
566+
return
567+
}
568+
569+
return {
570+
...req,
571+
requestContext: {
572+
...req.requestContext,
573+
transportName,
574+
cacheKey: `${req.requestContext.cacheKey}-fallback`,
575+
},
576+
} as AdapterRequest<EmptyInputParameters>
577+
}
547578
}

src/adapter/endpoint.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export class AdapterEndpoint<T extends EndpointGenerics> implements AdapterEndpo
4646
settings: T['Settings'],
4747
) => string
4848
defaultTransport?: string
49+
fallbackTransport?: string
4950

5051
constructor(params: AdapterEndpointParams<T>) {
5152
this.name = params.name
@@ -55,6 +56,7 @@ export class AdapterEndpoint<T extends EndpointGenerics> implements AdapterEndpo
5556
this.transportRoutes = params.transportRoutes
5657
this.customRouter = params.customRouter
5758
this.defaultTransport = params.defaultTransport
59+
this.fallbackTransport = params.fallbackTransport?.toLowerCase()
5860
} else {
5961
this.transportRoutes = new TransportRoutes<T>().register(
6062
DEFAULT_TRANSPORT_NAME,
@@ -186,6 +188,34 @@ export class AdapterEndpoint<T extends EndpointGenerics> implements AdapterEndpo
186188
return transportName
187189
}
188190

191+
getFallbackTransportNameForRequest(primaryTransportName: string, settings: T['Settings']) {
192+
if (!settings.TRANSPORT_FALLBACK_ENABLED || !this.fallbackTransport) {
193+
logger.trace('TRANSPORT_FALLBACK_ENABLED is false or fallbackTransport is not set')
194+
return
195+
}
196+
197+
const fallbackTransportName = this.fallbackTransport.toLowerCase()
198+
199+
if (!this.transportRoutes.get(fallbackTransportName)) {
200+
throw new AdapterError({
201+
statusCode: 400,
202+
message: `No fallback transport found for key "${fallbackTransportName}", must be one of ${JSON.stringify(
203+
this.transportRoutes.routeNames(),
204+
)}`,
205+
})
206+
}
207+
208+
if (fallbackTransportName === primaryTransportName.toLowerCase()) {
209+
throw new AdapterError({
210+
statusCode: 400,
211+
message: `Fallback transport "${fallbackTransportName}" cannot be the same as primary transport.`,
212+
})
213+
}
214+
215+
logger.debug(`Request can fall back to transport "${fallbackTransportName}"`)
216+
return fallbackTransportName
217+
}
218+
189219
/**
190220
* Default routing strategy. Will try and use the transport override if present
191221
* or transport input parameter in the request body.

src/adapter/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ type MultiTransportAdapterEndpointParams<T extends EndpointGenerics> = {
183183

184184
/** If no value is returned from the custom router or the default (transport param), which transport to use */
185185
defaultTransport?: string
186+
187+
/** Backup transport to use when the primary transport is unable to return data */
188+
fallbackTransport?: string
186189
}
187190

188191
/**

src/config/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,11 @@ export const BaseSettingsDefinition = {
276276
default: 200,
277277
validate: validator.integer({ min: 10, max: 1000 }),
278278
},
279+
TRANSPORT_FALLBACK_ENABLED: {
280+
description: 'Flag to enable endpoint fallback transports when configured',
281+
type: 'boolean',
282+
default: false,
283+
},
279284
DEFAULT_CACHE_KEY: {
280285
description: 'Default key to be used when one cannot be determined from request parameters',
281286
type: 'string',

test/transports/routing.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,75 @@ test.serial('transports with same name throws error', async (t) => {
680680
)
681681
})
682682

683+
test.serial('fallback transport is ignored when disabled', (t) => {
684+
const endpoint = new AdapterEndpoint<BaseEndpointTypes>({
685+
inputParameters,
686+
name: 'price',
687+
transportRoutes: transports,
688+
fallbackTransport: 'invalid',
689+
})
690+
const config = new AdapterConfig(settings)
691+
config.initialize()
692+
693+
t.is(endpoint.getFallbackTransportNameForRequest('batch', config.settings), undefined)
694+
})
695+
696+
test.serial('fallback transport is normalized when enabled', (t) => {
697+
const endpoint = new AdapterEndpoint<BaseEndpointTypes>({
698+
inputParameters,
699+
name: 'price',
700+
transportRoutes: transports,
701+
fallbackTransport: 'WEBSOCKET',
702+
})
703+
const config = new AdapterConfig(settings, {
704+
envDefaultOverrides: {
705+
TRANSPORT_FALLBACK_ENABLED: true,
706+
},
707+
})
708+
config.initialize()
709+
710+
t.is(endpoint.getFallbackTransportNameForRequest('batch', config.settings), 'websocket')
711+
})
712+
713+
test.serial('fallback transport must be registered when enabled', (t) => {
714+
const endpoint = new AdapterEndpoint<BaseEndpointTypes>({
715+
inputParameters,
716+
name: 'price',
717+
transportRoutes: transports,
718+
fallbackTransport: 'invalid',
719+
})
720+
const config = new AdapterConfig(settings, {
721+
envDefaultOverrides: {
722+
TRANSPORT_FALLBACK_ENABLED: true,
723+
},
724+
})
725+
config.initialize()
726+
727+
t.throws(() => endpoint.getFallbackTransportNameForRequest('batch', config.settings), {
728+
message:
729+
'No fallback transport found for key "invalid", must be one of ["websocket","batch","sse"]',
730+
})
731+
})
732+
733+
test.serial('fallback transport cannot match primary transport', (t) => {
734+
const endpoint = new AdapterEndpoint<BaseEndpointTypes>({
735+
inputParameters,
736+
name: 'price',
737+
transportRoutes: transports,
738+
fallbackTransport: 'batch',
739+
})
740+
const config = new AdapterConfig(settings, {
741+
envDefaultOverrides: {
742+
TRANSPORT_FALLBACK_ENABLED: true,
743+
},
744+
})
745+
config.initialize()
746+
747+
t.throws(() => endpoint.getFallbackTransportNameForRequest('batch', config.settings), {
748+
message: 'Fallback transport "batch" cannot be the same as primary transport.',
749+
})
750+
})
751+
683752
test.serial('transport override routes to correct Transport', async (t) => {
684753
axiosMock
685754
.onPost(`${restUrl}/price`, {

0 commit comments

Comments
 (0)