From c0c79a9e8e62b5ca3ad55e66561d34f7aaeb0585 Mon Sep 17 00:00:00 2001 From: Piotr Adamczyk Date: Thu, 11 Apr 2024 14:20:21 +0200 Subject: [PATCH] Remove MultiIndexer (moved to L2BEAT repo) (#174) --- packages/uif-example/src/Application.ts | 50 -- .../uif-example/src/prices/PriceConfig.ts | 4 - .../uif-example/src/prices/PriceIndexer.ts | 107 ---- .../src/prices/PriceIndexerRepository.ts | 19 - .../uif-example/src/prices/PriceRepository.ts | 38 -- .../uif-example/src/prices/PriceService.ts | 30 -- packages/uif/CHANGELOG.md | 6 + packages/uif/package.json | 2 +- packages/uif/src/index.ts | 2 - .../src/indexers/multi/MultiIndexer.test.ts | 503 ------------------ .../uif/src/indexers/multi/MultiIndexer.ts | 231 -------- .../indexers/multi/diffConfigurations.test.ts | 226 -------- .../src/indexers/multi/diffConfigurations.ts | 97 ---- .../uif/src/indexers/multi/toRanges.test.ts | 232 -------- packages/uif/src/indexers/multi/toRanges.ts | 44 -- packages/uif/src/indexers/multi/types.ts | 33 -- 16 files changed, 7 insertions(+), 1617 deletions(-) delete mode 100644 packages/uif-example/src/prices/PriceConfig.ts delete mode 100644 packages/uif-example/src/prices/PriceIndexer.ts delete mode 100644 packages/uif-example/src/prices/PriceIndexerRepository.ts delete mode 100644 packages/uif-example/src/prices/PriceRepository.ts delete mode 100644 packages/uif-example/src/prices/PriceService.ts delete mode 100644 packages/uif/src/indexers/multi/MultiIndexer.test.ts delete mode 100644 packages/uif/src/indexers/multi/MultiIndexer.ts delete mode 100644 packages/uif/src/indexers/multi/diffConfigurations.test.ts delete mode 100644 packages/uif/src/indexers/multi/diffConfigurations.ts delete mode 100644 packages/uif/src/indexers/multi/toRanges.test.ts delete mode 100644 packages/uif/src/indexers/multi/toRanges.ts delete mode 100644 packages/uif/src/indexers/multi/types.ts diff --git a/packages/uif-example/src/Application.ts b/packages/uif-example/src/Application.ts index 3088053d..19358b8a 100644 --- a/packages/uif-example/src/Application.ts +++ b/packages/uif-example/src/Application.ts @@ -5,10 +5,6 @@ import { BlockIndexerRepository } from './blocks/BlockIndexerRepository' import { BlockRepository } from './blocks/BlockRepository' import { BlockService } from './blocks/BlockService' import { HourlyIndexer } from './HourlyIndexer' -import { PriceIndexer } from './prices/PriceIndexer' -import { PriceIndexerRepository } from './prices/PriceIndexerRepository' -import { PriceRepository } from './prices/PriceRepository' -import { PriceService } from './prices/PriceService' import { msToHours, ONE_HOUR_MS } from './utils' export class Application { @@ -19,50 +15,6 @@ export class Application { const hourlyIndexer = new HourlyIndexer(logger) - const priceService = new PriceService(logger) - const priceRepository = new PriceRepository() - const priceIndexerRepository = new PriceIndexerRepository() - - const ethereumPriceIndexer = new PriceIndexer( - 'price-ethereum', - priceService, - priceRepository, - priceIndexerRepository, - hourlyIndexer, - logger, - [ - { - // could be a hash of properties & minHeight instead - id: 'eth-ethereum', - properties: { tokenSymbol: 'ETH', apiId: 'ethereum' }, - minHeight: msToHours(Date.now() - 48 * ONE_HOUR_MS), - maxHeight: null, - }, - { - id: 'weth-ethereum', - properties: { tokenSymbol: 'WETH', apiId: 'ethereum' }, - minHeight: msToHours(Date.now() - 32 * ONE_HOUR_MS), - maxHeight: null, - }, - ], - ) - const bitcoinPriceIndexer = new PriceIndexer( - 'price-bitcoin', - priceService, - priceRepository, - priceIndexerRepository, - hourlyIndexer, - logger, - [ - { - id: 'btc-bitcoin', - properties: { tokenSymbol: 'BTC', apiId: 'bitcoin' }, - minHeight: msToHours(Date.now() - 72 * ONE_HOUR_MS), - maxHeight: null, - }, - ], - ) - const blockService = new BlockService() const blockRepository = new BlockRepository() const blockIndexerRepository = new BlockIndexerRepository() @@ -79,8 +31,6 @@ export class Application { logger.for('Application').info('Starting') await hourlyIndexer.start() - await ethereumPriceIndexer.start() - await bitcoinPriceIndexer.start() await blockIndexer.start() logger.for('Application').info('Started') diff --git a/packages/uif-example/src/prices/PriceConfig.ts b/packages/uif-example/src/prices/PriceConfig.ts deleted file mode 100644 index 1dbab632..00000000 --- a/packages/uif-example/src/prices/PriceConfig.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface PriceConfig { - tokenSymbol: string - apiId: string -} diff --git a/packages/uif-example/src/prices/PriceIndexer.ts b/packages/uif-example/src/prices/PriceIndexer.ts deleted file mode 100644 index 55c73796..00000000 --- a/packages/uif-example/src/prices/PriceIndexer.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Logger } from '@l2beat/backend-tools' -import { - Configuration, - IndexerOptions, - MultiIndexer, - RemovalConfiguration, - SavedConfiguration, - UpdateConfiguration, -} from '@l2beat/uif' - -import { HourlyIndexer } from '../HourlyIndexer' -import { ONE_HOUR_MS } from '../utils' -import { PriceConfig } from './PriceConfig' -import { PriceIndexerRepository } from './PriceIndexerRepository' -import { PriceRepository } from './PriceRepository' -import { PriceService } from './PriceService' - -export class PriceIndexer extends MultiIndexer { - private readonly apiId: string - - constructor( - private readonly indexerId: string, - private readonly priceService: PriceService, - private readonly priceRepository: PriceRepository, - private readonly priceIndexerRepository: PriceIndexerRepository, - hourlyIndexer: HourlyIndexer, - logger: Logger, - configurations: Configuration[], - options?: IndexerOptions, - ) { - super(logger, [hourlyIndexer], configurations, options) - this.apiId = getCommonApiId(configurations) - } - - override async multiInitialize(): Promise[]> { - return this.priceIndexerRepository.load(this.indexerId) - } - - override async getSafeHeight(): Promise { - // we don't have a safe height for prices as they don't depend on parent indexers - return Promise.resolve(undefined) - } - - override setSafeHeight(_height: number): Promise { - // we don't have a safe height for prices as they don't depend on parent indexers - return Promise.resolve() - } - - override async multiUpdate( - from: number, - to: number, - configurations: UpdateConfiguration[], - ): Promise { - // we only query 24 hours at a time - const adjustedTo = Math.min(to, from + 23) - - const prices = await this.priceService.getHourlyPrices( - this.apiId, - from * ONE_HOUR_MS, - adjustedTo * ONE_HOUR_MS, - ) - - const dataToSave = configurations - // TODO: don't update currentHeight for configs that have data - // TODO: test data downloaded to middle of the range - .filter((c) => !c.hasData) - .flatMap((configuration) => { - return prices.map(({ timestamp, price }) => ({ - tokenSymbol: configuration.properties.tokenSymbol, - timestamp, - price, - })) - }) - await this.priceRepository.save(dataToSave) - - return adjustedTo - } - - override async removeData( - configurations: RemovalConfiguration[], - ): Promise { - for (const c of configurations) { - await this.priceRepository.deletePrices( - c.properties.tokenSymbol, - c.from, - c.to, - ) - } - } - - override async saveConfigurations( - configurations: SavedConfiguration[], - ): Promise { - return this.priceIndexerRepository.save(this.indexerId, configurations) - } -} - -function getCommonApiId(configurations: Configuration[]): string { - const apiId = configurations[0]?.properties.apiId - if (!apiId) { - throw new Error('At least one configuration is required') - } - if (configurations.some((c) => c.properties.apiId !== apiId)) { - throw new Error('All configurations must have the same apiId') - } - return apiId -} diff --git a/packages/uif-example/src/prices/PriceIndexerRepository.ts b/packages/uif-example/src/prices/PriceIndexerRepository.ts deleted file mode 100644 index 6fcac7fd..00000000 --- a/packages/uif-example/src/prices/PriceIndexerRepository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SavedConfiguration } from '@l2beat/uif' - -import { PriceConfig } from './PriceConfig' - -export class PriceIndexerRepository { - private data: Record[]> = {} - - async save( - indexerId: string, - configurations: SavedConfiguration[], - ): Promise { - this.data[indexerId] = configurations - return Promise.resolve() - } - - async load(indexerId: string): Promise[]> { - return Promise.resolve(this.data[indexerId] ?? []) - } -} diff --git a/packages/uif-example/src/prices/PriceRepository.ts b/packages/uif-example/src/prices/PriceRepository.ts deleted file mode 100644 index 2690a32c..00000000 --- a/packages/uif-example/src/prices/PriceRepository.ts +++ /dev/null @@ -1,38 +0,0 @@ -export class PriceRepository { - data = new Map>() - - async save( - prices: { tokenSymbol: string; timestamp: number; price: number }[], - ): Promise { - for (const { tokenSymbol, timestamp, price } of prices) { - const tokenPrices = - this.data.get(tokenSymbol) ?? new Map() - this.data.set(tokenSymbol, tokenPrices) - - tokenPrices.set(timestamp, price) - } - return Promise.resolve() - } - - async deletePrices( - tokenSymbol: string, - fromTimestampInclusive: number, - toTimestampInclusive: number, - ): Promise { - const tokenPrices = this.data.get(tokenSymbol) - if (!tokenPrices) { - return - } - - for (const [timestamp] of tokenPrices) { - if ( - timestamp >= fromTimestampInclusive && - timestamp <= toTimestampInclusive - ) { - tokenPrices.delete(timestamp) - } - } - - return Promise.resolve() - } -} diff --git a/packages/uif-example/src/prices/PriceService.ts b/packages/uif-example/src/prices/PriceService.ts deleted file mode 100644 index 0026a094..00000000 --- a/packages/uif-example/src/prices/PriceService.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Logger } from '@l2beat/backend-tools' -import { setTimeout } from 'timers/promises' - -import { ONE_HOUR_MS } from '../utils' - -export class PriceService { - constructor(private readonly logger: Logger) { - this.logger = logger.for(this) - } - - async getHourlyPrices( - apiId: string, - startHourInclusive: number, - endHourInclusive: number, - ): Promise<{ timestamp: number; price: number }[]> { - const prices: { timestamp: number; price: number }[] = [] - for (let t = startHourInclusive; t <= endHourInclusive; t += ONE_HOUR_MS) { - prices.push({ timestamp: t, price: Math.random() * 1000 }) - } - - await setTimeout(1000) - - this.logger.info('Fetched prices', { - apiId, - since: startHourInclusive, - count: prices.length, - }) - return prices - } -} diff --git a/packages/uif/CHANGELOG.md b/packages/uif/CHANGELOG.md index c1ac6140..1fe3ae26 100644 --- a/packages/uif/CHANGELOG.md +++ b/packages/uif/CHANGELOG.md @@ -1,5 +1,11 @@ # @l2beat/uif +## 0.5.0 + +### Minor Changes + +- cd6d192: Removed `MultiIndexer` + ## 0.4.0 ### Minor Changes diff --git a/packages/uif/package.json b/packages/uif/package.json index 09239e99..58a891bc 100644 --- a/packages/uif/package.json +++ b/packages/uif/package.json @@ -1,7 +1,7 @@ { "name": "@l2beat/uif", "description": "Universal Indexer Framework.", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "repository": "https://github.com/l2beat/tools", "bugs": { diff --git a/packages/uif/src/index.ts b/packages/uif/src/index.ts index 9d6cc6d1..6b295d6a 100644 --- a/packages/uif/src/index.ts +++ b/packages/uif/src/index.ts @@ -1,6 +1,4 @@ export * from './Indexer' export * from './indexers/ChildIndexer' -export * from './indexers/multi/MultiIndexer' -export * from './indexers/multi/types' export * from './indexers/RootIndexer' export * from './Retries' diff --git a/packages/uif/src/indexers/multi/MultiIndexer.test.ts b/packages/uif/src/indexers/multi/MultiIndexer.test.ts deleted file mode 100644 index 96f67814..00000000 --- a/packages/uif/src/indexers/multi/MultiIndexer.test.ts +++ /dev/null @@ -1,503 +0,0 @@ -import { Logger } from '@l2beat/backend-tools' -import { expect, mockFn } from 'earl' - -import { MultiIndexer } from './MultiIndexer' -import { - Configuration, - RemovalConfiguration, - SavedConfiguration, - UpdateConfiguration, -} from './types' - -describe(MultiIndexer.name, () => { - describe(MultiIndexer.prototype.initialize.name, () => { - it('calls multiInitialize and saves configurations', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 400), actual('b', 200, 500)], - [ - saved('a', 100, 400, 300), - saved('b', 200, 500, 300), - saved('c', 100, 300, 300), - ], - ) - - const newHeight = await testIndexer.initialize() - expect(newHeight).toEqual(300) - - expect(testIndexer.removeData).toHaveBeenOnlyCalledWith([ - removal('c', 100, 300), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenOnlyCalledWith([ - saved('a', 100, 400, 300), - saved('b', 200, 500, 300), - ]) - }) - - it('skips calling removeData if there is nothing to remove', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 400), actual('b', 200, 500)], - [saved('a', 100, 400, 400), saved('b', 200, 500, 500)], - ) - - const newHeight = await testIndexer.initialize() - expect(newHeight).toEqual(Infinity) - - expect(testIndexer.removeData).not.toHaveBeenCalled() - expect(testIndexer.saveConfigurations).toHaveBeenCalledWith([ - saved('a', 100, 400, 400), - saved('b', 200, 500, 500), - ]) - }) - - it('no synced data', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 400), actual('b', 200, null)], - [], - ) - - const newHeight = await testIndexer.initialize() - expect(newHeight).toEqual(99) - - expect(testIndexer.removeData).not.toHaveBeenCalled() - expect(testIndexer.saveConfigurations).toHaveBeenCalledWith([ - saved('a', 100, 400, null), - saved('b', 200, null, null), - ]) - }) - - it('mismatched min and max times', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 500), actual('b', 200, 400), actual('c', 300, null)], - [ - saved('a', 100, 400, 300), - saved('b', 100, 300, 300), - saved('c', 300, 400, 300), - ], - ) - - const newHeight = await testIndexer.initialize() - expect(newHeight).toEqual(300) - - expect(testIndexer.removeData).toHaveBeenCalledWith([ - removal('b', 100, 199), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenCalledWith([ - saved('a', 100, 500, 300), - saved('b', 200, 400, 300), - saved('c', 300, null, 300), - ]) - }) - - it('calls getters in order', async () => { - const calls: string[] = [] - const testIndexer = new TestMultiIndexer([], []) - testIndexer.multiInitialize = mockFn(async () => { - calls.push('multiInitialize') - return [] - }) - testIndexer.getInitialConfigurations = mockFn(() => { - calls.push('getInitialConfigurations') - return [] - }) - testIndexer.getSafeHeight = mockFn(() => { - calls.push('getSafeHeight') - return Promise.resolve(undefined) - }) - - await testIndexer.initialize() - - expect(calls).toEqual([ - 'multiInitialize', - 'getInitialConfigurations', - 'getSafeHeight', - ]) - }) - - it('getSafeHeight lower than saved configs', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200)], - [saved('a', 100, 200, 150)], - ) - testIndexer.getSafeHeight.resolvesTo(130) - expect(await testIndexer.initialize()).toEqual(130) - }) - - it('getSafeHeight higher than saved configs', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200)], - [saved('a', 100, 200, 150)], - ) - testIndexer.getSafeHeight.resolvesTo(160) - expect(await testIndexer.initialize()).toEqual(150) - }) - }) - - describe(MultiIndexer.prototype.update.name, () => { - it('calls multiUpdate with an early matching configuration', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 300, 400)], - [], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(100, 500) - - expect(newHeight).toEqual(200) - expect(testIndexer.multiUpdate).toHaveBeenOnlyCalledWith(100, 200, [ - update('a', 100, 200, false), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 200, 200), - saved('b', 300, 400, null), - ]) - }) - - it('calls multiUpdate with a late matching configuration', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 300, 400)], - [saved('a', 100, 200, 200)], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(300, 500) - - expect(newHeight).toEqual(400) - expect(testIndexer.multiUpdate).toHaveBeenOnlyCalledWith(300, 400, [ - update('b', 300, 400, false), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 200, 200), - saved('b', 300, 400, 400), - ]) - }) - - it('calls multiUpdate with two matching configurations', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 100, 400)], - [], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(100, 500) - - expect(newHeight).toEqual(200) - expect(testIndexer.multiUpdate).toHaveBeenOnlyCalledWith(100, 200, [ - update('a', 100, 200, false), - update('b', 100, 400, false), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 200, 200), - saved('b', 100, 400, 200), - ]) - }) - - it('calls multiUpdate with two middle matching configurations', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 400), actual('b', 200, 500)], - [saved('a', 100, 400, 300), saved('b', 200, 500, 300)], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(301, 600) - - expect(newHeight).toEqual(400) - expect(testIndexer.multiUpdate).toHaveBeenOnlyCalledWith(301, 400, [ - update('a', 100, 400, false), - update('b', 200, 500, false), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 400, 400), - saved('b', 200, 500, 400), - ]) - }) - - it('skips calling multiUpdate if we are too early', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 300, 400)], - [], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(0, 500) - - expect(newHeight).toEqual(99) - expect(testIndexer.multiUpdate).not.toHaveBeenCalled() - expect(testIndexer.saveConfigurations).toHaveBeenCalledTimes(1) - }) - - it('skips calling multiUpdate if we are too late', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 300, 400)], - [], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(401, 500) - - expect(newHeight).toEqual(500) - expect(testIndexer.multiUpdate).not.toHaveBeenCalled() - expect(testIndexer.saveConfigurations).toHaveBeenCalledTimes(1) - }) - - it('skips calling multiUpdate between configs', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 300, 400)], - [], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(201, 500) - - expect(newHeight).toEqual(299) - expect(testIndexer.multiUpdate).not.toHaveBeenCalled() - expect(testIndexer.saveConfigurations).toHaveBeenCalledTimes(1) - }) - - it('calls multiUpdate with a matching configuration with data', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 100, 400)], - [saved('a', 100, 200, 200)], - ) - await testIndexer.initialize() - - const newHeight = await testIndexer.update(100, 500) - - expect(newHeight).toEqual(200) - expect(testIndexer.multiUpdate).toHaveBeenOnlyCalledWith(100, 200, [ - update('a', 100, 200, true), - update('b', 100, 400, false), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 200, 200), - saved('b', 100, 400, 200), - ]) - }) - - it('multiple update calls', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 200), actual('b', 100, 400)], - [saved('a', 100, 200, 200)], - ) - await testIndexer.initialize() - - expect(await testIndexer.update(100, 500)).toEqual(200) - expect(testIndexer.multiUpdate).toHaveBeenNthCalledWith(1, 100, 200, [ - update('a', 100, 200, true), - update('b', 100, 400, false), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 200, 200), - saved('b', 100, 400, 200), - ]) - - // The same range. In real life might be a result of a parent reorg - // Invalidate is a no-op so we don't need to call it - expect(await testIndexer.update(100, 500)).toEqual(200) - expect(testIndexer.multiUpdate).toHaveBeenNthCalledWith(2, 100, 200, [ - update('a', 100, 200, true), - update('b', 100, 400, true), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(3, [ - saved('a', 100, 200, 200), - saved('b', 100, 400, 200), - ]) - - // Next range - expect(await testIndexer.update(201, 500)).toEqual(400) - expect(testIndexer.multiUpdate).toHaveBeenNthCalledWith(3, 201, 400, [ - update('b', 100, 400, false), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(4, [ - saved('a', 100, 200, 200), - saved('b', 100, 400, 400), - ]) - }) - - it('correctly updates currentHeight in saved configurations', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 500), actual('b', 100, 500), actual('c', 100, 500)], - [ - saved('a', 100, 500, null), - saved('b', 100, 500, 250), - saved('c', 100, 500, 500), - ], - ) - expect(await testIndexer.initialize()).toEqual(99) - - expect(await testIndexer.update(100, 500)).toEqual(250) - expect(testIndexer.multiUpdate).toHaveBeenNthCalledWith(1, 100, 250, [ - update('a', 100, 500, false), - update('b', 100, 500, true), - update('c', 100, 500, true), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 500, 250), - saved('b', 100, 500, 250), - saved('c', 100, 500, 500), - ]) - - expect(await testIndexer.update(251, 500)).toEqual(500) - expect(testIndexer.multiUpdate).toHaveBeenNthCalledWith(2, 251, 500, [ - update('a', 100, 500, false), - update('b', 100, 500, false), - update('c', 100, 500, true), - ]) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(3, [ - saved('a', 100, 500, 500), - saved('b', 100, 500, 500), - saved('c', 100, 500, 500), - ]) - }) - - it('gets configurations from different source', async () => { - const testIndexer = new TestMultiIndexer(undefined, []) - testIndexer.getInitialConfigurations = () => [ - actual('a', 100, null), - actual('b', 100, null), - ] - - const newHeight = await testIndexer.initialize() - expect(newHeight).toEqual(99) - }) - }) - - describe('multiUpdate', () => { - it('returns the currentHeight', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 300), actual('b', 100, 400)], - [saved('a', 100, 300, 200), saved('b', 100, 400, 200)], - ) - await testIndexer.initialize() - - testIndexer.multiUpdate.resolvesTo(200) - - const newHeight = await testIndexer.update(200, 500) - expect(newHeight).toEqual(200) - expect(testIndexer.saveConfigurations).toHaveBeenCalledTimes(1) - }) - - it('returns the targetHeight', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 300), actual('b', 100, 400)], - [saved('a', 100, 300, 200), saved('b', 100, 400, 200)], - ) - await testIndexer.initialize() - - testIndexer.multiUpdate.resolvesTo(300) - - const newHeight = await testIndexer.update(200, 300) - expect(newHeight).toEqual(300) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 300, 300), - saved('b', 100, 400, 300), - ]) - }) - - it('returns something in between', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 300), actual('b', 100, 400)], - [saved('a', 100, 300, 200), saved('b', 100, 400, 200)], - ) - await testIndexer.initialize() - - testIndexer.multiUpdate.resolvesTo(250) - - const newHeight = await testIndexer.update(200, 300) - expect(newHeight).toEqual(250) - expect(testIndexer.saveConfigurations).toHaveBeenNthCalledWith(2, [ - saved('a', 100, 300, 250), - saved('b', 100, 400, 250), - ]) - }) - - it('cannot return less than currentHeight', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 300), actual('b', 100, 400)], - [saved('a', 100, 300, 200), saved('b', 100, 400, 200)], - ) - await testIndexer.initialize() - - testIndexer.multiUpdate.resolvesTo(150) - - await expect(testIndexer.update(200, 300)).toBeRejectedWith( - /returned height must be between from and to/, - ) - }) - - it('cannot return more than targetHeight', async () => { - const testIndexer = new TestMultiIndexer( - [actual('a', 100, 300), actual('b', 100, 400)], - [saved('a', 100, 300, 200), saved('b', 100, 400, 200)], - ) - await testIndexer.initialize() - - testIndexer.multiUpdate.resolvesTo(350) - - await expect(testIndexer.update(200, 300)).toBeRejectedWith( - /returned height must be between from and to/, - ) - }) - }) -}) - -class TestMultiIndexer extends MultiIndexer { - constructor( - configurations: Configuration[] | undefined, - private readonly _saved: SavedConfiguration[], - ) { - super(Logger.SILENT, [], configurations) - } - - getSafeHeight = - mockFn['getSafeHeight']>().resolvesTo(undefined) - - setSafeHeight = - mockFn['setSafeHeight']>().resolvesTo(undefined) - - override multiInitialize(): Promise[]> { - return Promise.resolve(this._saved) - } - - multiUpdate = mockFn['multiUpdate']>((_, targetHeight) => - Promise.resolve(targetHeight), - ) - - removeData = mockFn['removeData']>().resolvesTo(undefined) - - saveConfigurations = - mockFn['saveConfigurations']>().resolvesTo(undefined) -} - -function actual( - id: string, - minHeight: number, - maxHeight: number | null, -): Configuration { - return { id, properties: null, minHeight, maxHeight } -} - -function saved( - id: string, - minHeight: number, - maxHeight: number | null, - currentHeight: number | null, -): SavedConfiguration { - return { id, properties: null, minHeight, maxHeight, currentHeight } -} - -function update( - id: string, - minHeight: number, - maxHeight: number | null, - hasData: boolean, -): UpdateConfiguration { - return { id, properties: null, minHeight, maxHeight, hasData } -} - -function removal( - id: string, - from: number, - to: number, -): RemovalConfiguration { - return { id, properties: null, from, to } -} diff --git a/packages/uif/src/indexers/multi/MultiIndexer.ts b/packages/uif/src/indexers/multi/MultiIndexer.ts deleted file mode 100644 index 28c26ca4..00000000 --- a/packages/uif/src/indexers/multi/MultiIndexer.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { Logger } from '@l2beat/backend-tools' - -import { Indexer, IndexerOptions } from '../../Indexer' -import { ChildIndexer } from '../ChildIndexer' -import { diffConfigurations } from './diffConfigurations' -import { toRanges } from './toRanges' -import { - Configuration, - ConfigurationRange, - RemovalConfiguration, - SavedConfiguration, - UpdateConfiguration, -} from './types' - -export abstract class MultiIndexer extends ChildIndexer { - private ranges: ConfigurationRange[] = [] - private configurations: Configuration[] = [] - private saved: SavedConfiguration[] = [] - - constructor( - logger: Logger, - parents: Indexer[], - configurations?: Configuration[], - options?: IndexerOptions, - ) { - super(logger, parents, options) - if (configurations) { - this.configurations = configurations - } - } - - /** - * This will run as the first step of initialize() function. - * Allow overriding to provide configurations from a different source. - * Example: your configurations have autoincrement id, so you need to - * first add them to the database to get the MultiIndexer logic to work (it assumes every - * configuration has a unique id) - * @returns The configurations that the indexer should use to sync data. - */ - getInitialConfigurations(): Promise[]> | Configuration[] { - return this.configurations - } - - /** - * Initializes the indexer. It returns the configurations that were saved - * previously. In case no configurations were saved, it should return an empty - * array. - * - * This method is expected to read the configurations that was saved - * previously with `setStoredConfigurations`. It shouldn't call - * `setStoredConfigurations` itself. - * - * @returns The configurations that were saved previously. - */ - abstract multiInitialize(): Promise[]> - - /** - * Implements the main data fetching process. It is up to the indexer to - * decide how much data to fetch. For example given `.update(100, 200, [...])`, the - * indexer can only fetch data up to 110 and return 110. The next time this - * method will be called with `.update(110, 200, [...])`. - * - * @param from The height for which the indexer should start syncing data. - * This value is inclusive. If the indexer hasn't synced anything previously - * this will equal the minimum height of all configurations. - * - * @param to The height at which the indexer should end syncing data. This - * value is also inclusive so the indexer should eventually sync data for this - * height. - * - * @param configurations The configurations that the indexer should use to - * sync data. The configurations are guaranteed to be in the range of - * `from` and `to`. Some of those configurations might have been synced - * previously for this range. Those configurations will include the `hasData` - * flag set to `true`. - * - * @returns The height that the indexer has synced up to. Returning - * `from` means that the indexer has synced a single data point. Returning - * a value greater than `from` means that the indexer has synced up - * to that height. Returning a value less than `from` or greater than - * `to` is not permitted. - */ - abstract multiUpdate( - from: number, - to: number, - configurations: UpdateConfiguration[], - ): Promise - - /** - * Removes data that was previously synced but because configurations changed - * is no longer valid. The data should be removed for the ranges specified - * in each configuration. It is possible for multiple ranges to share a - * configuration id! - * - * This method can only be called during the initialization of the indexer, - * after `multiInitialize` returns. - */ - abstract removeData(configurations: RemovalConfiguration[]): Promise - - /** - * Saves configurations that the indexer should use to sync data. The - * configurations saved here should be read in the `multiInitialize` method. - * - * @param configurations The configurations that the indexer should save. The - * indexer should save the returned configurations and ensure that no other - * configurations are persisted. - */ - abstract saveConfigurations( - configurations: SavedConfiguration[], - ): Promise - - /** - * It should return a height that the indexer has synced up to. If the indexer - * has not synced any data, it should return `undefined`. - * - * This method is expected to read the height that was saved previously with - * `setSafeHeight`. It shouldn't call `setSafeHeight` itself. - * - * @returns The height that the indexer has synced up to. - */ - abstract getSafeHeight(): Promise - - async initialize(): Promise { - const previouslySaved = await this.multiInitialize() - - this.configurations = await this.getInitialConfigurations() - this.ranges = toRanges(this.configurations) - - const { toRemove, toSave, safeHeight } = diffConfigurations( - this.configurations, - previouslySaved, - ) - const oldSafeHeight = (await this.getSafeHeight()) ?? safeHeight - - this.saved = toSave - if (toRemove.length > 0) { - await this.removeData(toRemove) - } - await this.saveConfigurations(toSave) - - return Math.min(safeHeight, oldSafeHeight) - } - - async update(from: number, to: number): Promise { - const range = findRange(this.ranges, from) - if (range.configurations.length === 0) { - return Math.min(range.to, to) - } - - const { configurations, minCurrentHeight } = getConfigurationsInRange( - range, - this.saved, - from, - ) - const adjustedTo = Math.min(range.to, to, minCurrentHeight) - - this.logger.info('Calling multiUpdate', { - from, - to: adjustedTo, - configurations: configurations.length, - }) - const newHeight = await this.multiUpdate(from, adjustedTo, configurations) - if (newHeight < from || newHeight > adjustedTo) { - throw new Error( - 'Programmer error, returned height must be between from and to (both inclusive).', - ) - } - - if (newHeight > from) { - this.updateSavedConfigurations(configurations, newHeight) - await this.saveConfigurations(this.saved) - } - - return newHeight - } - - private updateSavedConfigurations( - updatedConfigurations: UpdateConfiguration[], - newHeight: number, - ): void { - for (const updated of updatedConfigurations) { - const saved = this.saved.find((c) => c.id === updated.id) - if (!saved) { - throw new Error('Programmer error, saved configuration not found') - } - if (saved.currentHeight === null || saved.currentHeight < newHeight) { - saved.currentHeight = newHeight - } - } - } - - async invalidate(targetHeight: number): Promise { - return Promise.resolve(targetHeight) - } -} - -function findRange( - ranges: ConfigurationRange[], - from: number, -): ConfigurationRange { - const range = ranges.find((range) => range.from <= from && range.to >= from) - if (!range) { - throw new Error('Programmer error, there should always be a range') - } - return range -} - -function getConfigurationsInRange( - range: ConfigurationRange, - savedConfigurations: SavedConfiguration[], - currentHeight: number, -): { configurations: UpdateConfiguration[]; minCurrentHeight: number } { - let minCurrentHeight = Infinity - const configurations = range.configurations.map( - (configuration): UpdateConfiguration => { - const saved = savedConfigurations.find((c) => c.id === configuration.id) - if ( - // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - saved && - saved.currentHeight !== null && - saved.currentHeight > currentHeight - ) { - minCurrentHeight = Math.min(minCurrentHeight, saved.currentHeight) - return { ...configuration, hasData: true } - } else { - return { ...configuration, hasData: false } - } - }, - ) - return { configurations, minCurrentHeight } -} diff --git a/packages/uif/src/indexers/multi/diffConfigurations.test.ts b/packages/uif/src/indexers/multi/diffConfigurations.test.ts deleted file mode 100644 index bb745745..00000000 --- a/packages/uif/src/indexers/multi/diffConfigurations.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { expect } from 'earl' - -import { diffConfigurations } from './diffConfigurations' -import { - Configuration, - RemovalConfiguration, - SavedConfiguration, -} from './types' - -describe(diffConfigurations.name, () => { - describe('errors', () => { - it('duplicate config id', () => { - expect(() => - diffConfigurations([actual('a', 100, null), actual('a', 200, 300)], []), - ).toThrow(/a is duplicated/) - }) - - it('minHeight greater than maxHeight', () => { - expect(() => diffConfigurations([actual('a', 200, 100)], [])).toThrow( - /a has minHeight greater than maxHeight/, - ) - }) - }) - - describe('regular sync', () => { - it('empty actual and stored', () => { - const result = diffConfigurations([], []) - expect(result).toEqual({ toRemove: [], toSave: [], safeHeight: Infinity }) - }) - - it('empty stored', () => { - const result = diffConfigurations( - [actual('a', 100, null), actual('b', 200, 300)], - [], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, null, null), saved('b', 200, 300, null)], - safeHeight: 99, - }) - }) - - it('partially synced, both early', () => { - const result = diffConfigurations( - [actual('a', 100, 400), actual('b', 200, null)], - [saved('a', 100, 400, 300), saved('b', 200, null, 300)], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, 400, 300), saved('b', 200, null, 300)], - safeHeight: 300, - }) - }) - - it('partially synced, one new not yet started', () => { - const result = diffConfigurations( - [actual('a', 100, 400), actual('b', 555, null)], - [saved('a', 100, 400, 300), saved('b', 555, null, null)], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, 400, 300), saved('b', 555, null, null)], - safeHeight: 300, - }) - }) - - it('partially synced, one finished', () => { - const result = diffConfigurations( - [actual('a', 100, 555), actual('b', 200, 300)], - [saved('a', 100, 555, 400), saved('b', 200, 300, 300)], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, 555, 400), saved('b', 200, 300, 300)], - safeHeight: 400, - }) - }) - - it('partially synced, one finished, one infinite', () => { - const result = diffConfigurations( - [actual('a', 100, null), actual('b', 200, 300)], - [saved('a', 100, null, 400), saved('b', 200, 300, 300)], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, null, 400), saved('b', 200, 300, 300)], - safeHeight: 400, - }) - }) - - it('both synced', () => { - const result = diffConfigurations( - [actual('a', 100, 400), actual('b', 200, 300)], - [saved('a', 100, 400, 400), saved('b', 200, 300, 300)], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, 400, 400), saved('b', 200, 300, 300)], - safeHeight: Infinity, - }) - }) - }) - - describe('configuration changed', () => { - it('empty actual', () => { - const result = diffConfigurations( - [], - [saved('a', 100, 400, 300), saved('b', 200, null, 300)], - ) - expect(result).toEqual({ - toRemove: [removal('a', 100, 300), removal('b', 200, 300)], - toSave: [], - safeHeight: Infinity, - }) - }) - - it('single removed', () => { - const result = diffConfigurations( - [actual('b', 200, 400)], - [saved('a', 100, null, 300), saved('b', 200, 400, 300)], - ) - expect(result).toEqual({ - toRemove: [removal('a', 100, 300)], - toSave: [saved('b', 200, 400, 300)], - safeHeight: 300, - }) - }) - - it('maxHeight updated up', () => { - const result = diffConfigurations( - [actual('a', 100, 400)], - [saved('a', 100, 300, 300)], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, 400, 300)], - safeHeight: 300, - }) - }) - - it('maxHeight updated down', () => { - const result = diffConfigurations( - [actual('a', 100, 200)], - [saved('a', 100, 300, 300)], - ) - expect(result).toEqual({ - toRemove: [removal('a', 201, 300)], - toSave: [saved('a', 100, 200, 200)], - safeHeight: Infinity, - }) - }) - - it('maxHeight removed', () => { - const result = diffConfigurations( - [actual('a', 100, null)], - [saved('a', 100, 300, 300)], - ) - expect(result).toEqual({ - toRemove: [], - toSave: [saved('a', 100, null, 300)], - safeHeight: 300, - }) - }) - - it('minHeight updated up', () => { - const result = diffConfigurations( - [actual('a', 200, 400)], - [saved('a', 100, 400, 300)], - ) - expect(result).toEqual({ - toRemove: [removal('a', 100, 199)], - toSave: [saved('a', 200, 400, 300)], - safeHeight: 300, - }) - }) - - it('minHeight updated down', () => { - const result = diffConfigurations( - [actual('a', 100, 400)], - [saved('a', 200, 400, 300)], - ) - expect(result).toEqual({ - toRemove: [removal('a', 200, 300)], - toSave: [saved('a', 100, 400, null)], - safeHeight: 99, - }) - }) - - it('both min and max height updated', () => { - const result = diffConfigurations( - [actual('a', 200, 300)], - [saved('a', 100, 400, 400)], - ) - expect(result).toEqual({ - toRemove: [removal('a', 100, 199), removal('a', 301, 400)], - toSave: [saved('a', 200, 300, 300)], - safeHeight: Infinity, - }) - }) - }) -}) - -function actual( - id: string, - minHeight: number, - maxHeight: number | null, -): Configuration { - return { id, properties: null, minHeight, maxHeight } -} - -function saved( - id: string, - minHeight: number, - maxHeight: number | null, - currentHeight: number | null, -): SavedConfiguration { - return { id, properties: null, minHeight, maxHeight, currentHeight } -} - -function removal( - id: string, - from: number, - to: number, -): RemovalConfiguration { - return { id, properties: null, from, to } -} diff --git a/packages/uif/src/indexers/multi/diffConfigurations.ts b/packages/uif/src/indexers/multi/diffConfigurations.ts deleted file mode 100644 index 92bba679..00000000 --- a/packages/uif/src/indexers/multi/diffConfigurations.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - Configuration, - RemovalConfiguration, - SavedConfiguration, -} from './types' - -export function diffConfigurations( - actual: Configuration[], - saved: SavedConfiguration[], -): { - toRemove: RemovalConfiguration[] - toSave: SavedConfiguration[] - safeHeight: number -} { - let safeHeight = Infinity - - const actualMap = new Map(actual.map((c) => [c.id, c])) - const savedMap = new Map(saved.map((c) => [c.id, c])) - - const toRemove: RemovalConfiguration[] = [] - for (const c of saved) { - if (actualMap.has(c.id) || c.currentHeight === null) { - continue - } - toRemove.push({ - id: c.id, - properties: c.properties, - from: c.minHeight, - to: c.currentHeight, - }) - } - - const toSave: SavedConfiguration[] = [] - - const knownIds = new Set() - for (const c of actual) { - if (knownIds.has(c.id)) { - throw new Error(`Configuration ${c.id} is duplicated!`) - } - knownIds.add(c.id) - - if (c.maxHeight !== null && c.minHeight > c.maxHeight) { - throw new Error( - `Configuration ${c.id} has minHeight greater than maxHeight!`, - ) - } - - const stored = savedMap.get(c.id) - if (!stored || stored.currentHeight === null) { - safeHeight = Math.min(safeHeight, c.minHeight - 1) - toSave.push({ ...c, currentHeight: null }) - continue - } - - if (stored.minHeight > c.minHeight) { - safeHeight = Math.min(safeHeight, c.minHeight - 1) - // We remove everything because we cannot have gaps in downloaded data - // We will re-download everything from the beginning - toRemove.push({ - id: stored.id, - properties: stored.properties, - from: stored.minHeight, - to: stored.currentHeight, - }) - toSave.push({ ...c, currentHeight: null }) - continue - } - - if (stored.minHeight < c.minHeight) { - toRemove.push({ - id: stored.id, - properties: stored.properties, - from: stored.minHeight, - to: c.minHeight - 1, - }) - } - - if (c.maxHeight !== null && stored.currentHeight > c.maxHeight) { - toRemove.push({ - id: stored.id, - properties: stored.properties, - from: c.maxHeight + 1, - to: stored.currentHeight, - }) - } else if (c.maxHeight === null || stored.currentHeight < c.maxHeight) { - safeHeight = Math.min(safeHeight, stored.currentHeight) - } - - const currentHeight = Math.min( - stored.currentHeight, - c.maxHeight ?? stored.currentHeight, - ) - toSave.push({ ...c, currentHeight }) - } - - return { toRemove, toSave, safeHeight } -} diff --git a/packages/uif/src/indexers/multi/toRanges.test.ts b/packages/uif/src/indexers/multi/toRanges.test.ts deleted file mode 100644 index f27b189f..00000000 --- a/packages/uif/src/indexers/multi/toRanges.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { expect } from 'earl' - -import { toRanges } from './toRanges' -import { Configuration } from './types' - -describe(toRanges.name, () => { - it('empty', () => { - const ranges = toRanges([]) - expect(ranges).toEqual([ - { from: -Infinity, to: Infinity, configurations: [] }, - ]) - }) - - it('single infinite configuration', () => { - const ranges = toRanges([actual('a', 100, null)]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: Infinity, configurations: [actual('a', 100, null)] }, - ]) - }) - - it('single finite configuration', () => { - const ranges = toRanges([actual('a', 100, 300)]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: 300, configurations: [actual('a', 100, 300)] }, - { from: 301, to: Infinity, configurations: [] }, - ]) - }) - - it('multiple overlapping configurations on the edges', () => { - const ranges = toRanges([actual('a', 100, 300), actual('b', 300, 500)]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: 299, configurations: [actual('a', 100, 300)] }, - { - from: 300, - to: 300, - configurations: [actual('a', 100, 300), actual('b', 300, 500)], - }, - { from: 301, to: 500, configurations: [actual('b', 300, 500)] }, - { from: 501, to: Infinity, configurations: [] }, - ]) - }) - - it('multiple overlapping configurations', () => { - const ranges = toRanges([ - actual('a', 100, 300), - actual('b', 200, 400), - actual('c', 300, 500), - ]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: 199, configurations: [actual('a', 100, 300)] }, - { - from: 200, - to: 299, - configurations: [actual('a', 100, 300), actual('b', 200, 400)], - }, - { - from: 300, - to: 300, - configurations: [ - actual('a', 100, 300), - actual('b', 200, 400), - actual('c', 300, 500), - ], - }, - { - from: 301, - to: 400, - configurations: [actual('b', 200, 400), actual('c', 300, 500)], - }, - { from: 401, to: 500, configurations: [actual('c', 300, 500)] }, - { from: 501, to: Infinity, configurations: [] }, - ]) - }) - - it('multiple non-overlapping configurations', () => { - const ranges = toRanges([ - actual('a', 100, 200), - actual('b', 300, 400), - actual('c', 500, 600), - ]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: 200, configurations: [actual('a', 100, 200)] }, - { from: 201, to: 299, configurations: [] }, - { from: 300, to: 400, configurations: [actual('b', 300, 400)] }, - { from: 401, to: 499, configurations: [] }, - { from: 500, to: 600, configurations: [actual('c', 500, 600)] }, - { from: 601, to: Infinity, configurations: [] }, - ]) - }) - - it('multiple overlapping and non-overlapping configurations', () => { - const ranges = toRanges([ - actual('a', 100, 200), - actual('b', 300, 500), - actual('c', 400, 600), - actual('d', 700, 800), - ]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: 200, configurations: [actual('a', 100, 200)] }, - { from: 201, to: 299, configurations: [] }, - { from: 300, to: 399, configurations: [actual('b', 300, 500)] }, - { - from: 400, - to: 500, - configurations: [actual('b', 300, 500), actual('c', 400, 600)], - }, - { from: 501, to: 600, configurations: [actual('c', 400, 600)] }, - { from: 601, to: 699, configurations: [] }, - { from: 700, to: 800, configurations: [actual('d', 700, 800)] }, - { from: 801, to: Infinity, configurations: [] }, - ]) - }) - - it('adjacent: one configuration start where other ends', () => { - const ranges = toRanges([actual('a', 100, 200), actual('b', 200, 300)]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: 199, configurations: [actual('a', 100, 200)] }, - { - from: 200, - to: 200, - configurations: [actual('a', 100, 200), actual('b', 200, 300)], - }, - { - from: 201, - to: 300, - configurations: [actual('b', 200, 300)], - }, - { - from: 301, - to: Infinity, - configurations: [], - }, - ]) - }) - - it('identical: two configurations with exactly the same boundaries', () => { - const ranges = toRanges([actual('a', 100, 200), actual('b', 100, 200)]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { - from: 100, - to: 200, - configurations: [actual('a', 100, 200), actual('b', 100, 200)], - }, - { - from: 201, - to: Infinity, - configurations: [], - }, - ]) - }) - - it('single point: configuration starts and ends in the same time', () => { - const ranges = toRanges([actual('a', 100, 100)]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { - from: 100, - to: 100, - configurations: [actual('a', 100, 100)], - }, - { - from: 101, - to: Infinity, - configurations: [], - }, - ]) - }) - - it('order of inputs does not affect output', () => { - const ranges = toRanges([ - actual('b', 300, 400), - actual('c', 500, 600), - actual('a', 100, 200), - ]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { from: 100, to: 200, configurations: [actual('a', 100, 200)] }, - { from: 201, to: 299, configurations: [] }, - { from: 300, to: 400, configurations: [actual('b', 300, 400)] }, - { from: 401, to: 499, configurations: [] }, - { from: 500, to: 600, configurations: [actual('c', 500, 600)] }, - { from: 601, to: Infinity, configurations: [] }, - ]) - }) - - it('same starting point, multiple maxHeights', () => { - const ranges = toRanges([ - actual('a', 100, 200), - actual('b', 100, 300), - actual('c', 100, 400), - ]) - expect(ranges).toEqual([ - { from: -Infinity, to: 99, configurations: [] }, - { - from: 100, - to: 200, - configurations: [ - actual('a', 100, 200), - actual('b', 100, 300), - actual('c', 100, 400), - ], - }, - { - from: 201, - to: 300, - configurations: [actual('b', 100, 300), actual('c', 100, 400)], - }, - { - from: 301, - to: 400, - configurations: [actual('c', 100, 400)], - }, - { from: 401, to: Infinity, configurations: [] }, - ]) - }) -}) - -function actual( - id: string, - minHeight: number, - maxHeight: number | null, -): Configuration { - return { id, properties: null, minHeight, maxHeight } -} diff --git a/packages/uif/src/indexers/multi/toRanges.ts b/packages/uif/src/indexers/multi/toRanges.ts deleted file mode 100644 index a99e94b2..00000000 --- a/packages/uif/src/indexers/multi/toRanges.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Configuration, ConfigurationRange } from './types' - -export function toRanges( - configurations: Configuration[], -): ConfigurationRange[] { - const minHeights = configurations.map((c) => c.minHeight) - const maxHeights = configurations - .map((c) => c.maxHeight) - .filter((height): height is number => height !== null) - - const starts = minHeights - .concat(maxHeights.map((height) => height + 1)) - .sort((a, b) => a - b) - .filter((height, i, arr) => arr.indexOf(height) === i) - - let lastRange: ConfigurationRange = { - from: -Infinity, - to: Infinity, - configurations: [], - } - const ranges: ConfigurationRange[] = [lastRange] - for (const start of starts) { - lastRange.to = start - 1 - lastRange = { - from: start, - to: Infinity, - configurations: [], - } - ranges.push(lastRange) - } - - for (const configuration of configurations) { - const min = configuration.minHeight - const max = configuration.maxHeight ?? Infinity - - for (const range of ranges) { - if (!(max < range.from || min > range.to)) { - range.configurations.push(configuration) - } - } - } - - return ranges -} diff --git a/packages/uif/src/indexers/multi/types.ts b/packages/uif/src/indexers/multi/types.ts deleted file mode 100644 index b2079f5f..00000000 --- a/packages/uif/src/indexers/multi/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -export interface Configuration { - id: string - properties: T - /** Inclusive */ - minHeight: number - /** Inclusive */ - maxHeight: number | null -} - -export interface UpdateConfiguration extends Configuration { - hasData: boolean -} - -export interface SavedConfiguration extends Configuration { - currentHeight: number | null -} - -export interface RemovalConfiguration { - id: string - properties: T - /** Inclusive */ - from: number - /** Inclusive */ - to: number -} - -export interface ConfigurationRange { - /** Inclusive */ - from: number - /** Inclusive */ - to: number - configurations: Configuration[] -}