Resolve "feat(Rates): Composite Adapter should be able to add/remove adapters"
This commit is contained in:
@@ -1,28 +1,32 @@
|
||||
import { BaseRates } from './base-rates.js';
|
||||
import { UUIDV7 } from '../utils/uuid.js';
|
||||
|
||||
/**
|
||||
* A composite rate adapter that combines rates from multiple rate sources
|
||||
* using a configurable policy.
|
||||
*/
|
||||
export class RatesComposite extends BaseRates {
|
||||
// List of the adapters with a unique ID for each adapter
|
||||
private adapters: { id: string; adapter: BaseRates }[] = [];
|
||||
|
||||
// Track available pairs across all rate adapters
|
||||
private availablePairs: Set<string> = new Set();
|
||||
|
||||
// Cache of the latest rates from each adapter
|
||||
private rateCache: Map<string, Map<string, Map<number, number>>> = new Map();
|
||||
private rateCache: Map<string, Map<string, Map<string, number>>> = new Map();
|
||||
|
||||
// Off callbacks for unsubscribing from events
|
||||
private unsubscribeFunctions: (() => void)[] = [];
|
||||
private unsubscribeFunctions: Map<string, () => void> = new Map();
|
||||
|
||||
/**
|
||||
* Creates a new RatesComposite instance
|
||||
*
|
||||
* @param adapters Array of BaseRates adapters to combine
|
||||
* @param policy Policy to determine how to combine rates (defaults to average)
|
||||
* @param refreshIntervalMs How often to emit combined rates (defaults to 1000ms)
|
||||
* @param refreshIntervalMs How often to emit combined rates (defaults to 60000ms)
|
||||
*/
|
||||
constructor(
|
||||
private adapters: BaseRates[],
|
||||
adapters: BaseRates[],
|
||||
private policy: RatesCompositePolicy = ratesAveragePolicy,
|
||||
private refreshIntervalMs: number = 60000,
|
||||
) {
|
||||
@@ -30,34 +34,17 @@ export class RatesComposite extends BaseRates {
|
||||
|
||||
// Initialize rate cache
|
||||
this.rateCache = new Map();
|
||||
|
||||
// Add all adapters to the composite
|
||||
adapters.forEach((adapter) => this.addAdapter(adapter, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all rate adapters and setup event listeners
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Setup event listeners for all adapters
|
||||
for (let i = 0; i < this.adapters.length; i++) {
|
||||
const adapter = this.adapters[i];
|
||||
const adapterIndex = i;
|
||||
|
||||
const listener = (event: {
|
||||
denominatorUnitCode: string;
|
||||
numeratorUnitCode: string;
|
||||
price: number;
|
||||
}) => {
|
||||
this.handleRateUpdate(adapterIndex, event);
|
||||
};
|
||||
|
||||
// Use the returned unsubscribe function from the custom EventEmitter
|
||||
const unsubscribe = adapter.on('rateUpdated', listener);
|
||||
|
||||
// Store unsubscribe function for cleanup
|
||||
this.unsubscribeFunctions.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Start all adapters
|
||||
await Promise.all(this.adapters.map((adapter) => adapter.start()));
|
||||
await Promise.all(this.adapters.map(({ adapter }) => adapter.start()));
|
||||
|
||||
// Start periodic refresh to emit combined rates
|
||||
this.startPeriodicRefresh();
|
||||
@@ -74,13 +61,86 @@ export class RatesComposite extends BaseRates {
|
||||
}
|
||||
|
||||
// Clean up all event listeners by calling their unsubscribe functions
|
||||
for (const unsubscribe of this.unsubscribeFunctions) {
|
||||
for (const unsubscribe of this.unsubscribeFunctions.values()) {
|
||||
unsubscribe();
|
||||
}
|
||||
this.unsubscribeFunctions = [];
|
||||
this.unsubscribeFunctions.clear();
|
||||
|
||||
// Stop all adapters
|
||||
await Promise.all(this.adapters.map((adapter) => adapter.stop()));
|
||||
await Promise.all(this.adapters.map(({ adapter }) => adapter.stop()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new rate adapter to the composite
|
||||
*/
|
||||
public addAdapter(adapter: BaseRates, start: boolean = true): void {
|
||||
// Generate a random ID for the adapter
|
||||
const adapterId = UUIDV7.generateRandom().toString();
|
||||
|
||||
// Add the adapter to the list
|
||||
this.adapters.push({ id: adapterId, adapter });
|
||||
|
||||
const listener = (event: {
|
||||
denominatorUnitCode: string;
|
||||
numeratorUnitCode: string;
|
||||
price: number;
|
||||
}) => {
|
||||
this.handleRateUpdate(adapterId, event);
|
||||
};
|
||||
|
||||
// Use the returned unsubscribe function from the custom EventEmitter
|
||||
const unsubscribe = adapter.on('rateUpdated', listener);
|
||||
|
||||
// Store unsubscribe function associated with the adapter ID
|
||||
this.unsubscribeFunctions.set(adapterId, unsubscribe);
|
||||
|
||||
// Start the adapter if requested
|
||||
if (start) {
|
||||
adapter.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a rate adapter from the composite
|
||||
*/
|
||||
public removeAdapter(adapter: BaseRates, stop: boolean = true): void {
|
||||
// Get the index of the adapter
|
||||
const adapterId = this.adapters.find(
|
||||
({ adapter: storedAdapter }) => storedAdapter === adapter,
|
||||
)?.id;
|
||||
|
||||
// If the adapter is not found, do nothing
|
||||
if (!adapterId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the unsubscribe function for the adapter
|
||||
this.unsubscribeFunctions.get(adapterId)?.();
|
||||
|
||||
// Remove the adapter's rates from the cache
|
||||
this.rateCache.forEach((numeratorMap) => {
|
||||
numeratorMap.forEach((rateMap) => {
|
||||
rateMap.delete(adapterId);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove the unsubscribe function for the adapter
|
||||
this.unsubscribeFunctions.delete(adapterId);
|
||||
|
||||
// Remove the adapter from the list
|
||||
this.adapters = this.adapters.filter(({ id }) => id !== adapterId);
|
||||
|
||||
// Stop the adapter if requested
|
||||
if (stop) {
|
||||
adapter.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the policy for how to combine rates
|
||||
*/
|
||||
public setPolicy(policy: RatesCompositePolicy): void {
|
||||
this.policy = policy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +148,9 @@ export class RatesComposite extends BaseRates {
|
||||
*/
|
||||
public async listPairs(): Promise<Set<string>> {
|
||||
// Get available pairs from all adapters
|
||||
const pairPromises = this.adapters.map((adapter) => adapter.listPairs());
|
||||
const pairPromises = this.adapters.map(({ adapter }) =>
|
||||
adapter.listPairs(),
|
||||
);
|
||||
const pairSets = await Promise.all(pairPromises);
|
||||
|
||||
// Combine all pairs
|
||||
@@ -127,7 +189,7 @@ export class RatesComposite extends BaseRates {
|
||||
* Handles a rate update from an adapter
|
||||
*/
|
||||
private handleRateUpdate(
|
||||
adapterIndex: number,
|
||||
adapterId: string,
|
||||
event: {
|
||||
denominatorUnitCode: string;
|
||||
numeratorUnitCode: string;
|
||||
@@ -148,7 +210,7 @@ export class RatesComposite extends BaseRates {
|
||||
|
||||
// Store the rate in the cache
|
||||
const rateMap = denomMap.get(numeratorUnitCode)!;
|
||||
rateMap.set(adapterIndex, price);
|
||||
rateMap.set(adapterId, price);
|
||||
|
||||
// Add pair to available pairs
|
||||
this.availablePairs.add(`${numeratorUnitCode}/${denominatorUnitCode}`);
|
||||
@@ -162,8 +224,10 @@ export class RatesComposite extends BaseRates {
|
||||
for (const [denominator, numeratorMap] of this.rateCache.entries()) {
|
||||
// Process each numerator currency
|
||||
for (const [numerator, rateMap] of numeratorMap.entries()) {
|
||||
// Get all rates for this pair
|
||||
const rates = Array.from(rateMap.values());
|
||||
// Go through our adapters in order, plucking the rate for each adapter
|
||||
const rates = this.adapters
|
||||
.map(({ id }) => rateMap.get(id))
|
||||
.filter((rate) => rate !== undefined);
|
||||
|
||||
// Apply policy to get combined rate
|
||||
const combinedRate = this.policy(rates);
|
||||
|
||||
Reference in New Issue
Block a user