Skip to content

Commit d131af9

Browse files
authored
Merge pull request #281 from bancorprotocol/improved-init
improved init behavior
2 parents 325a7da + c6e166b commit d131af9

File tree

6 files changed

+141
-38
lines changed

6 files changed

+141
-38
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@bancor/carbon-sdk",
33
"type": "module",
44
"source": "src/index.ts",
5-
"version": "0.0.119-DEV",
5+
"version": "0.0.120-DEV",
66
"description": "The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around our matching algorithm, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades",
77
"main": "dist/index.cjs",
88
"module": "dist/index.js",

src/chain-cache/ChainCache.ts

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
5656
private _latestTradesByDirectedPair: { [key: string]: TradeData } = {};
5757
private _blocksMetadata: BlockMetadata[] = [];
5858
private _tradingFeePPMByPair: { [key: string]: number } = {};
59+
private _isCacheInitialized: boolean = false;
60+
private _handleCacheMiss:
61+
| ((token0: string, token1: string) => Promise<void>)
62+
| undefined;
5963
//#endregion private members
6064

6165
//#region serialization for persistent caching
@@ -173,6 +177,25 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
173177
}
174178
//#endregion serialization for persistent caching
175179

180+
public setCacheMissHandler(
181+
handler: (token0: string, token1: string) => Promise<void>
182+
): void {
183+
this._handleCacheMiss = handler;
184+
}
185+
186+
private async _checkAndHandleCacheMiss(token0: string, token1: string) {
187+
if (
188+
this._isCacheInitialized ||
189+
!this._handleCacheMiss ||
190+
this.hasCachedPair(token0, token1)
191+
)
192+
return;
193+
194+
logger.debug('Cache miss for pair', token0, token1);
195+
await this._handleCacheMiss(token0, token1);
196+
logger.debug('Cache miss for pair', token0, token1, 'resolved');
197+
}
198+
176199
public clear(silent: boolean = false): void {
177200
const pairs = Object.keys(this._strategiesByPair).map(fromPairKey);
178201
this._strategiesByPair = {};
@@ -193,6 +216,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
193216
token0: string,
194217
token1: string
195218
): Promise<EncodedStrategy[] | undefined> {
219+
await this._checkAndHandleCacheMiss(token0, token1);
196220
const key = toPairKey(token0, token1);
197221
return this._strategiesByPair[key];
198222
}
@@ -238,6 +262,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
238262
targetToken: string,
239263
keepNonTradable: boolean = false
240264
): Promise<OrdersMap> {
265+
await this._checkAndHandleCacheMiss(sourceToken, targetToken);
241266
const key = toDirectionKey(sourceToken, targetToken);
242267
const orders = this._ordersByDirectedPair[key] || {};
243268

@@ -257,6 +282,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
257282
token0: string,
258283
token1: string
259284
): Promise<TradeData | undefined> {
285+
await this._checkAndHandleCacheMiss(token0, token1);
260286
const key = toPairKey(token0, token1);
261287
return this._latestTradesByPair[key];
262288
}
@@ -265,6 +291,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
265291
sourceToken: string,
266292
targetToken: string
267293
): Promise<TradeData | undefined> {
294+
await this._checkAndHandleCacheMiss(sourceToken, targetToken);
268295
const key = toDirectionKey(sourceToken, targetToken);
269296
return this._latestTradesByDirectedPair[key];
270297
}
@@ -281,6 +308,7 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
281308
token0: string,
282309
token1: string
283310
): Promise<number | undefined> {
311+
await this._checkAndHandleCacheMiss(token0, token1);
284312
const key = toPairKey(token0, token1);
285313
return this._tradingFeePPMByPair[key];
286314
}
@@ -295,23 +323,10 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
295323
//#endregion public getters
296324

297325
//#region cache updates
298-
/**
299-
* This method is to be used when all the existing strategies of a pair are
300-
* fetched and are to be stored in the cache.
301-
* Once a pair is cached, the only way to update it is by using `applyEvents`.
302-
* If all the strategies of a pair are deleted, the pair remains in the cache and there's
303-
* no need to add it again.
304-
* @param {string} token0 - address of the first token of the pair
305-
* @param {string} token1 - address of the second token of the pair
306-
* @param {EncodedStrategy[]} strategies - the strategies to be cached
307-
* @throws {Error} if the pair is already cached
308-
* @returns {void}
309-
*/
310-
public addPair(
326+
private _addPair(
311327
token0: string,
312328
token1: string,
313-
strategies: EncodedStrategy[],
314-
noPairAddedEvent: boolean = false
329+
strategies: EncodedStrategy[]
315330
): void {
316331
logger.debug(
317332
'Adding pair with',
@@ -329,9 +344,56 @@ export class ChainCache extends (EventEmitter as new () => TypedEventEmitter<Cac
329344
this._strategiesById[strategy.id.toString()] = strategy;
330345
this._addStrategyOrders(strategy);
331346
});
332-
if (!noPairAddedEvent) {
333-
logger.debug('Emitting onPairAddedToCache', token0, token1);
334-
this.emit('onPairAddedToCache', fromPairKey(key));
347+
}
348+
349+
/**
350+
* This method is to be used when all the existing strategies of a pair are
351+
* fetched and are to be stored in the cache.
352+
* Once a pair is cached, the only way to update it is by using `applyEvents`.
353+
* If all the strategies of a pair are deleted, the pair remains in the cache and there's
354+
* no need to add it again.
355+
* It emits an event `onPairAddedToCache` with the pair info.
356+
* @param {string} token0 - address of the first token of the pair
357+
* @param {string} token1 - address of the second token of the pair
358+
* @param {EncodedStrategy[]} strategies - the strategies to be cached
359+
* @throws {Error} if the pair is already cached
360+
* @returns {void}
361+
* @emits {onPairAddedToCache} - when the pair is added to the cache
362+
*/
363+
public addPair(
364+
token0: string,
365+
token1: string,
366+
strategies: EncodedStrategy[]
367+
): void {
368+
this._addPair(token0, token1, strategies);
369+
logger.debug('Emitting onPairAddedToCache', token0, token1);
370+
this.emit('onPairAddedToCache', fromPairKey(toPairKey(token0, token1)));
371+
}
372+
373+
/**
374+
* This method is used when a number of pairs are fetched and are to be stored in the cache.
375+
* If this is the first time it is called with a non empty list of pairs it emits an event
376+
* to let know that the cache was initialized with data for the first time.
377+
*
378+
* @param {Array<{pair: TokenPair, strategies: EncodedStrategy[]}>} pairs - the pairs to add to the cache
379+
* @emits {onCacheInitialized} - when the cache is initialized with data for the first time
380+
* @throws {Error} if any pair is already cached
381+
* @returns {void}
382+
*/
383+
public bulkAddPairs(
384+
pairs: {
385+
pair: TokenPair;
386+
strategies: EncodedStrategy[];
387+
}[]
388+
): void {
389+
logger.debug('Bulk adding pairs', pairs);
390+
for (const pair of pairs) {
391+
this._addPair(pair.pair[0], pair.pair[1], pair.strategies);
392+
}
393+
if (pairs.length > 0 && !this._isCacheInitialized) {
394+
this._isCacheInitialized = true;
395+
logger.debug('Emitting onCacheInitialized');
396+
this.emit('onCacheInitialized');
335397
}
336398
}
337399

src/chain-cache/ChainSync.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export class ChainSync {
181181
this._setTimeout(processPairs, 60000);
182182
}
183183
};
184-
this._setTimeout(processPairs, 1);
184+
await processPairs();
185185
}
186186

187187
private async _syncPairDataBatch(): Promise<void> {
@@ -201,21 +201,7 @@ export class ChainSync {
201201
batches.map((batch) => this._fetcher.strategiesByPairs(batch))
202202
);
203203
logger.debug('_syncPairDataBatch strategiesBatches', strategiesBatches);
204-
strategiesBatches.flat().forEach((pairStrategies) => {
205-
logger.debug(
206-
'_syncPairDataBatch adding pair',
207-
pairStrategies.pair[0],
208-
pairStrategies.pair[1],
209-
'with strategies',
210-
pairStrategies.strategies
211-
);
212-
this._chainCache.addPair(
213-
pairStrategies.pair[0],
214-
pairStrategies.pair[1],
215-
pairStrategies.strategies,
216-
true
217-
);
218-
});
204+
this._chainCache.bulkAddPairs(strategiesBatches.flat());
219205
this._uncachedPairs = [];
220206
} catch (error) {
221207
logger.error('Failed to fetch strategies for pairs batch:', error);
@@ -229,9 +215,18 @@ export class ChainSync {
229215
'ChainSync.startDataSync() must be called before syncPairData()'
230216
);
231217
}
232-
const strategies = await this._fetcher.strategiesByPair(token0, token1);
233-
if (this._chainCache.hasCachedPair(token0, token1)) return;
234-
this._chainCache.addPair(token0, token1, strategies, false);
218+
try {
219+
const strategies = await this._fetcher.strategiesByPair(token0, token1);
220+
if (this._chainCache.hasCachedPair(token0, token1)) return;
221+
this._chainCache.addPair(token0, token1, strategies);
222+
} catch (error) {
223+
logger.error(
224+
'Failed to fetch strategies for pair:',
225+
token0,
226+
token1,
227+
error
228+
);
229+
}
235230
}
236231

237232
private async _syncEvents(): Promise<void> {

src/chain-cache/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ export const initSyncedCache = (
4343
msToWaitBetweenSyncs,
4444
chunkSize
4545
);
46+
cache.setCacheMissHandler(syncer.syncPairData.bind(syncer));
4647
return { cache, startDataSync: syncer.startDataSync.bind(syncer) };
4748
};

src/chain-cache/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type EventMap = {
88
export type CacheEvents = {
99
onPairDataChanged: (affectedPairs: TokenPair[]) => void;
1010
onPairAddedToCache: (addedPair: TokenPair) => void;
11+
onCacheInitialized: () => void;
1112
};
1213

1314
export interface TypedEventEmitter<Events extends EventMap> {

tests/ChainCache.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,20 @@ describe('ChainCache', () => {
179179
});
180180
});
181181
describe('onChange', () => {
182+
it('should fire onCacheInitialized event only once when cache is initialized', async () => {
183+
const cache = new ChainCache();
184+
let eventCounter = 0;
185+
cache.on('onCacheInitialized', () => {
186+
eventCounter++;
187+
});
188+
cache.bulkAddPairs([
189+
{ pair: ['abc', 'xyz'], strategies: [encodedStrategy1] },
190+
]);
191+
cache.bulkAddPairs([
192+
{ pair: ['def', 'ghi'], strategies: [encodedStrategy2] },
193+
]);
194+
expect(eventCounter).to.equal(1);
195+
});
182196
it('should fire onPairAddedToCache event when pair is added', async () => {
183197
const cache = new ChainCache();
184198
let affectedPair: TokenPair = ['', ''];
@@ -337,4 +351,34 @@ describe('ChainCache', () => {
337351
expect(await cache.getTradingFeePPMByPair('xyz', 'abc')).to.equal(13);
338352
});
339353
});
354+
describe('cache miss', () => {
355+
it('getStrategiesByPair call miss handler when pair is not cached', async () => {
356+
const cache = new ChainCache();
357+
let missHandlerCalled = false;
358+
cache.setCacheMissHandler(async (token0, token1) => {
359+
missHandlerCalled = true;
360+
expect([token0, token1]).to.deep.equal(['abc', 'xyz']);
361+
});
362+
await cache.getStrategiesByPair('abc', 'xyz');
363+
expect(missHandlerCalled).to.be.true;
364+
});
365+
it('getOrdersByPair call miss handler when pair is not cached', async () => {
366+
const cache = new ChainCache();
367+
let missHandlerCalled = false;
368+
cache.setCacheMissHandler(async (token0, token1) => {
369+
missHandlerCalled = true;
370+
expect([token0, token1]).to.deep.equal(['abc', 'xyz']);
371+
});
372+
await cache.getOrdersByPair('abc', 'xyz');
373+
expect(missHandlerCalled).to.be.true;
374+
});
375+
it('getStrategiesByPair calls miss handler, which adds the missing pair, allowing the call to return strategies', async () => {
376+
const cache = new ChainCache();
377+
cache.setCacheMissHandler(async (token0, token1) => {
378+
cache.addPair(token0, token1, [encodedStrategy1]);
379+
});
380+
const strategies = await cache.getStrategiesByPair('abc', 'xyz');
381+
expect(strategies).to.deep.equal([encodedStrategy1]);
382+
});
383+
});
340384
});

0 commit comments

Comments
 (0)