diff --git a/packages/extension/src/pages/ibc-swap/select-asset/index.tsx b/packages/extension/src/pages/ibc-swap/select-asset/index.tsx index f6c4a979a7..e5537ddf44 100644 --- a/packages/extension/src/pages/ibc-swap/select-asset/index.tsx +++ b/packages/extension/src/pages/ibc-swap/select-asset/index.tsx @@ -34,7 +34,7 @@ class IBCSwapDestinationState { // 현재 가지고 있지 않은 자산도 유저가 선택할 수 있도록 UI 상 후순위로 둔채로 balance 등을 보여주지 않은채 선택은 할 수 있도록 보여준다. @computed get tokens(): { - tokens: ViewToken[]; + tokens: ReadonlyArray; remaining: { currency: Currency; chainInfo: IChainInfoImpl; diff --git a/packages/extension/src/stores/huge-queries/index.ts b/packages/extension/src/stores/huge-queries/index.ts index 1c134deeff..3091b49a82 100644 --- a/packages/extension/src/stores/huge-queries/index.ts +++ b/packages/extension/src/stores/huge-queries/index.ts @@ -8,9 +8,10 @@ import { QueryError, } from "@keplr-wallet/stores"; import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit"; -import { computed, makeObservable } from "mobx"; +import { action, autorun, computed } from "mobx"; import { DenomHelper } from "@keplr-wallet/common"; import { computedFn } from "mobx-utils"; +import { BinarySortArray } from "./sort"; interface ViewToken { chainInfo: IChainInfoImpl; @@ -30,25 +31,95 @@ interface ViewToken { export class HugeQueriesStore { protected static zeroDec = new Dec(0); + protected balanceBinarySort: BinarySortArray; + protected delegationBinarySort: BinarySortArray; + protected unbondingBinarySort: BinarySortArray<{ + viewToken: ViewToken; + completeTime: string; + }>; + protected claimableRewardsBinarySort: BinarySortArray; + constructor( protected readonly chainStore: ChainStore, protected readonly queriesStore: IQueriesStore, protected readonly accountStore: IAccountStore, protected readonly priceStore: CoinGeckoPriceStore ) { - makeObservable(this); + let balanceDisposal: (() => void) | undefined; + this.balanceBinarySort = new BinarySortArray( + this.sortByPrice, + () => { + balanceDisposal = autorun(() => { + this.updateBalances(); + }); + }, + () => { + if (balanceDisposal) { + balanceDisposal(); + } + } + ); + let delegationDisposal: (() => void) | undefined; + this.delegationBinarySort = new BinarySortArray( + this.sortByPrice, + () => { + delegationDisposal = autorun(() => { + this.updateDelegations(); + }); + }, + () => { + if (delegationDisposal) { + delegationDisposal(); + } + } + ); + let unbondingDisposal: (() => void) | undefined; + this.unbondingBinarySort = new BinarySortArray<{ + viewToken: ViewToken; + completeTime: string; + }>( + (a, b) => { + return this.sortByPrice(a.viewToken, b.viewToken); + }, + () => { + unbondingDisposal = autorun(() => { + this.updateUnbondings(); + }); + }, + () => { + if (unbondingDisposal) { + unbondingDisposal(); + } + } + ); + let claimableRewardsDisposal: (() => void) | undefined; + this.claimableRewardsBinarySort = new BinarySortArray( + this.sortByPrice, + () => { + claimableRewardsDisposal = autorun(() => { + this.updateClaimableRewards(); + }); + }, + () => { + if (claimableRewardsDisposal) { + claimableRewardsDisposal(); + } + } + ); } - // Key: {chainIdentifier}/{coinMinimalDenom} - @computed - protected get allKnownBalancesMap(): Map { - const map = new Map(); + @action + protected updateBalances() { + const keysUsed = new Map(); + const prevKeyMap = new Map(this.balanceBinarySort.indexForKeyMap()); for (const chainInfo of this.chainStore.chainInfosInUI) { const account = this.accountStore.getAccount(chainInfo.chainId); + if (account.bech32Address === "") { continue; } + const queries = this.queriesStore.get(chainInfo.chainId); const currencies = [...chainInfo.currencies]; if (chainInfo.stakeCurrency) { @@ -56,7 +127,6 @@ export class HugeQueriesStore { } for (const currency of currencies) { const denomHelper = new DenomHelper(currency.coinMinimalDenom); - const queries = this.queriesStore.get(chainInfo.chainId); const queryBalance = this.chainStore.isEvmChain(chainInfo.chainId) && denomHelper.type === "erc20" @@ -68,7 +138,7 @@ export class HugeQueriesStore { ); const key = `${chainInfo.chainIdentifier}/${currency.coinMinimalDenom}`; - if (!map.has(key)) { + if (!keysUsed.get(key)) { if ( chainInfo.stakeCurrency?.coinMinimalDenom === currency.coinMinimalDenom @@ -83,7 +153,9 @@ export class HugeQueriesStore { // continue; // } - map.set(key, { + keysUsed.set(key, true); + prevKeyMap.delete(key); + this.balanceBinarySort.pushAndSort(key, { chainInfo, token: balance, price: currency.coinGeckoId @@ -103,7 +175,9 @@ export class HugeQueriesStore { continue; } - map.set(key, { + keysUsed.set(key, true); + prevKeyMap.delete(key); + this.balanceBinarySort.pushAndSort(key, { chainInfo, token: balance.balance, price: currency.coinGeckoId @@ -118,56 +192,43 @@ export class HugeQueriesStore { } } - return map; + for (const removedKey of prevKeyMap.keys()) { + this.balanceBinarySort.remove(removedKey); + } } @computed - get allKnownBalances(): ViewToken[] { - return Array.from(this.allKnownBalancesMap.values()); + get allKnownBalances(): ReadonlyArray { + return this.balanceBinarySort.arr; } - protected sortByPrice = (a: ViewToken, b: ViewToken) => { - const aPrice = - this.priceStore.calculatePrice(a.token)?.toDec() ?? - HugeQueriesStore.zeroDec; - const bPrice = - this.priceStore.calculatePrice(b.token)?.toDec() ?? - HugeQueriesStore.zeroDec; - - if (aPrice.equals(bPrice)) { - return 0; - } else if (aPrice.gt(bPrice)) { - return -1; - } else { - return 1; - } - }; - - getAllBalances = computedFn((allowIBCToken: boolean): ViewToken[] => { - const res: ViewToken[] = []; - for (const chainInfo of this.chainStore.chainInfosInUI) { - for (const currency of chainInfo.currencies) { - const denomHelper = new DenomHelper(currency.coinMinimalDenom); - if ( - !allowIBCToken && - denomHelper.type === "native" && - denomHelper.denom.startsWith("ibc/") - ) { - continue; - } + getAllBalances = computedFn( + (allowIBCToken: boolean): ReadonlyArray => { + const keys: Map = new Map(); + for (const chainInfo of this.chainStore.chainInfosInUI) { + for (const currency of chainInfo.currencies) { + const denomHelper = new DenomHelper(currency.coinMinimalDenom); + if ( + !allowIBCToken && + denomHelper.type === "native" && + denomHelper.denom.startsWith("ibc/") + ) { + continue; + } - const key = `${chainInfo.chainIdentifier}/${currency.coinMinimalDenom}`; - const viewToken = this.allKnownBalancesMap.get(key); - if (viewToken) { - res.push(viewToken); + const key = `${chainInfo.chainIdentifier}/${currency.coinMinimalDenom}`; + keys.set(key, true); } } + return this.balanceBinarySort.arr.filter((viewToken) => { + const key = viewToken[BinarySortArray.SymbolKey]; + return keys.get(key); + }); } - return res.sort(this.sortByPrice); - }); + ); filterLowBalanceTokens = computedFn( - (viewTokens: ViewToken[]): ViewToken[] => { + (viewTokens: ReadonlyArray): ViewToken[] => { return viewTokens.filter((viewToken) => { // Hide the unknown ibc tokens. if ( @@ -195,23 +256,23 @@ export class HugeQueriesStore { @computed get stakables(): ViewToken[] { - const res: ViewToken[] = []; + const keys: Map = new Map(); for (const chainInfo of this.chainStore.chainInfosInUI) { if (!chainInfo.stakeCurrency) { continue; } const key = `${chainInfo.chainIdentifier}/${chainInfo.stakeCurrency.coinMinimalDenom}`; - const viewToken = this.allKnownBalancesMap.get(key); - if (viewToken) { - res.push(viewToken); - } + keys.set(key, true); } - return res.sort(this.sortByPrice); + return this.balanceBinarySort.arr.filter((viewToken) => { + const key = viewToken[BinarySortArray.SymbolKey]; + return keys.get(key); + }); } @computed get notStakbles(): ViewToken[] { - const res: ViewToken[] = []; + const keys: Map = new Map(); for (const chainInfo of this.chainStore.chainInfosInUI) { for (const currency of chainInfo.currencies) { if ( @@ -229,18 +290,18 @@ export class HugeQueriesStore { } const key = `${chainInfo.chainIdentifier}/${currency.coinMinimalDenom}`; - const viewToken = this.allKnownBalancesMap.get(key); - if (viewToken) { - res.push(viewToken); - } + keys.set(key, true); } } - return res.sort(this.sortByPrice); + return this.balanceBinarySort.arr.filter((viewToken) => { + const key = viewToken[BinarySortArray.SymbolKey]; + return keys.get(key); + }); } @computed get ibcTokens(): ViewToken[] { - const res: ViewToken[] = []; + const keys: Map = new Map(); for (const chainInfo of this.chainStore.chainInfosInUI) { for (const currency of chainInfo.currencies) { const denomHelper = new DenomHelper(currency.coinMinimalDenom); @@ -249,19 +310,20 @@ export class HugeQueriesStore { denomHelper.denom.startsWith("ibc/") ) { const key = `${chainInfo.chainIdentifier}/${currency.coinMinimalDenom}`; - const viewToken = this.allKnownBalancesMap.get(key); - if (viewToken) { - res.push(viewToken); - } + keys.set(key, true); } } } - return res.sort(this.sortByPrice); + return this.balanceBinarySort.arr.filter((viewToken) => { + const key = viewToken[BinarySortArray.SymbolKey]; + return keys.get(key); + }); } - @computed - get delegations(): ViewToken[] { - const res: ViewToken[] = []; + @action + protected updateDelegations(): void { + const prevKeyMap = new Map(this.delegationBinarySort.indexForKeyMap()); + for (const chainInfo of this.chainStore.chainInfosInUI) { const account = this.accountStore.getAccount(chainInfo.chainId); if (account.bech32Address === "") { @@ -272,12 +334,13 @@ export class HugeQueriesStore { queries.cosmos.queryDelegations.getQueryBech32Address( account.bech32Address ); - if (!queryDelegation.total) { continue; } - res.push({ + const key = `${chainInfo.chainId}/${account.bech32Address}`; + prevKeyMap.delete(key); + this.delegationBinarySort.pushAndSort(key, { chainInfo, token: queryDelegation.total, price: this.priceStore.calculatePrice(queryDelegation.total), @@ -285,18 +348,21 @@ export class HugeQueriesStore { error: queryDelegation.error, }); } - return res.sort(this.sortByPrice); + + for (const removedKey of prevKeyMap.keys()) { + this.delegationBinarySort.remove(removedKey); + } } @computed - get unbondings(): { - viewToken: ViewToken; - completeTime: string; - }[] { - const res: { - viewToken: ViewToken; - completeTime: string; - }[] = []; + get delegations(): ReadonlyArray { + return this.delegationBinarySort.arr; + } + + @action + protected updateUnbondings(): void { + const prevKeyMap = new Map(this.unbondingBinarySort.indexForKeyMap()); + for (const chainInfo of this.chainStore.chainInfosInUI) { const account = this.accountStore.getAccount(chainInfo.chainId); if (account.bech32Address === "") { @@ -308,8 +374,10 @@ export class HugeQueriesStore { account.bech32Address ); - for (const unbonding of queryUnbonding.unbondings) { - for (const entry of unbonding.entries) { + for (let i = 0; i < queryUnbonding.unbondings.length; i++) { + const unbonding = queryUnbonding.unbondings[i]; + for (let j = 0; j < unbonding.entries.length; j++) { + const entry = unbonding.entries[j]; if (!chainInfo.stakeCurrency) { continue; } @@ -317,7 +385,10 @@ export class HugeQueriesStore { chainInfo.stakeCurrency, entry.balance ); - res.push({ + + const key = `${chainInfo.chainId}/${account.bech32Address}/${i}/${j}`; + prevKeyMap.delete(key); + this.unbondingBinarySort.pushAndSort(key, { viewToken: { chainInfo, token: balance, @@ -330,6 +401,72 @@ export class HugeQueriesStore { } } } - return res; + + for (const removedKey of prevKeyMap.keys()) { + this.unbondingBinarySort.remove(removedKey); + } + } + + @computed + get unbondings(): ReadonlyArray<{ + viewToken: ViewToken; + completeTime: string; + }> { + return this.unbondingBinarySort.arr; + } + + @action + protected updateClaimableRewards(): void { + const prevKeyMap = new Map( + this.claimableRewardsBinarySort.indexForKeyMap() + ); + + for (const chainInfo of this.chainStore.chainInfosInUI) { + const account = this.accountStore.getAccount(chainInfo.chainId); + if (account.bech32Address === "") { + continue; + } + const queries = this.queriesStore.get(chainInfo.chainId); + const queryRewards = queries.cosmos.queryRewards.getQueryBech32Address( + account.bech32Address + ); + + if ( + queryRewards.stakableReward && + queryRewards.stakableReward.toDec().gt(new Dec(0)) + ) { + const key = `${chainInfo.chainId}/${account.bech32Address}`; + prevKeyMap.delete(key); + this.claimableRewardsBinarySort.pushAndSort(key, { + chainInfo, + token: queryRewards.stakableReward, + price: this.priceStore.calculatePrice(queryRewards.stakableReward), + isFetching: queryRewards.isFetching, + error: queryRewards.error, + }); + } + } + + for (const removedKey of prevKeyMap.keys()) { + this.claimableRewardsBinarySort.remove(removedKey); + } + } + + @computed + get claimableRewards(): ReadonlyArray { + return this.claimableRewardsBinarySort.arr; + } + + protected sortByPrice(a: ViewToken, b: ViewToken): number { + const aPrice = a.price?.toDec() ?? HugeQueriesStore.zeroDec; + const bPrice = b.price?.toDec() ?? HugeQueriesStore.zeroDec; + + if (aPrice.equals(bPrice)) { + return 0; + } else if (aPrice.gt(bPrice)) { + return -1; + } else { + return 1; + } } } diff --git a/packages/extension/src/stores/huge-queries/sort.spec.ts b/packages/extension/src/stores/huge-queries/sort.spec.ts new file mode 100644 index 0000000000..76d0c53212 --- /dev/null +++ b/packages/extension/src/stores/huge-queries/sort.spec.ts @@ -0,0 +1,362 @@ +import { BinarySortArray } from "./sort"; + +describe("Test BinarySortArray", () => { + test("only push sort test", async () => { + const sort = new BinarySortArray<{ + value: number; + }>( + (a, b) => { + const r = a.value - b.value; + if (Math.abs(r) <= 0.1 + Number.EPSILON) { + return 0; + } + return r; + }, + () => { + // noop + }, + () => { + // noop + } + ); + + sort.pushAndSort("2", { + value: 2, + }); + expect(sort.indexOf("2")).toBe(0); + expect(sort.arr.map((v) => v.value)).toStrictEqual([2]); + + sort.pushAndSort("2.1", { + value: 2.1, + }); + expect(sort.indexOf("2")).toBe(0); + expect(sort.indexOf("2.1")).toBe(1); + expect(sort.arr.map((v) => v.value)).toStrictEqual([2, 2.1]); + + sort.pushAndSort("1", { + value: 1, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.arr.map((v) => v.value)).toStrictEqual([1, 2, 2.1]); + + sort.pushAndSort("3", { + value: 3, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.arr.map((v) => v.value)).toStrictEqual([1, 2, 2.1, 3]); + + sort.pushAndSort("3.1", { + value: 3.1, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.arr.map((v) => v.value)).toStrictEqual([1, 2, 2.1, 3, 3.1]); + + sort.pushAndSort("3.05", { + value: 3.05, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1, 2, 2.1, 3, 3.1, 3.05, + ]); + + sort.pushAndSort("2.5", { + value: 2.5, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("2.5")).toBe(3); + expect(sort.indexOf("3")).toBe(4); + expect(sort.indexOf("3.1")).toBe(5); + expect(sort.indexOf("3.05")).toBe(6); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1, 2, 2.1, 2.5, 3, 3.1, 3.05, + ]); + }); + + test("sort existing key test", async () => { + const sort = new BinarySortArray<{ + value: number; + }>( + (a, b) => { + const r = a.value - b.value; + if (Math.abs(r) <= 0.1 + Number.EPSILON) { + return 0; + } + return r; + }, + () => { + // noop + }, + () => { + // noop + } + ); + + sort.pushAndSort("2", { + value: 2, + }); + sort.pushAndSort("2.1", { + value: 2.1, + }); + sort.pushAndSort("1", { + value: 1, + }); + sort.pushAndSort("3", { + value: 3, + }); + sort.pushAndSort("3.1", { + value: 3.1, + }); + sort.pushAndSort("3.05", { + value: 3.05, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1, 2, 2.1, 3, 3.1, 3.05, + ]); + + sort.pushAndSort("3.1", { + value: 3.1, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1, 2, 2.1, 3, 3.1, 3.05, + ]); + + sort.pushAndSort("3.05", { + value: 3.05, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1, 2, 2.1, 3, 3.1, 3.05, + ]); + + sort.pushAndSort("1", { + value: 1, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1, 2, 2.1, 3, 3.1, 3.05, + ]); + + sort.pushAndSort("1", { + value: 1.5, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1.5, 2, 2.1, 3, 3.1, 3.05, + ]); + + sort.pushAndSort("3.05", { + value: 3, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1.5, 2, 2.1, 3, 3.1, 3, + ]); + + sort.pushAndSort("2", { + value: 3.05, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2.1")).toBe(1); + expect(sort.indexOf("3")).toBe(2); + expect(sort.indexOf("3.1")).toBe(3); + expect(sort.indexOf("3.05")).toBe(4); + expect(sort.indexOf("2")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1.5, 2.1, 3, 3.1, 3, 3.05, + ]); + + sort.pushAndSort("2", { + value: 4, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2.1")).toBe(1); + expect(sort.indexOf("3")).toBe(2); + expect(sort.indexOf("3.1")).toBe(3); + expect(sort.indexOf("3.05")).toBe(4); + expect(sort.indexOf("2")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1.5, 2.1, 3, 3.1, 3, 4, + ]); + + sort.pushAndSort("3.05", { + value: 3.5, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2.1")).toBe(1); + expect(sort.indexOf("3")).toBe(2); + expect(sort.indexOf("3.1")).toBe(3); + expect(sort.indexOf("3.05")).toBe(4); + expect(sort.indexOf("2")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1.5, 2.1, 3, 3.1, 3.5, 4, + ]); + + sort.pushAndSort("2.1", { + value: 3.25, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("3")).toBe(1); + expect(sort.indexOf("3.1")).toBe(2); + expect(sort.indexOf("2.1")).toBe(3); + expect(sort.indexOf("3.05")).toBe(4); + expect(sort.indexOf("2")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1.5, 3, 3.1, 3.25, 3.5, 4, + ]); + + sort.pushAndSort("2.1", { + value: 2.1, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2.1")).toBe(1); + expect(sort.indexOf("3")).toBe(2); + expect(sort.indexOf("3.1")).toBe(3); + expect(sort.indexOf("3.05")).toBe(4); + expect(sort.indexOf("2")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1.5, 2.1, 3, 3.1, 3.5, 4, + ]); + + sort.pushAndSort("1", { + value: 5, + }); + expect(sort.indexOf("2.1")).toBe(0); + expect(sort.indexOf("3")).toBe(1); + expect(sort.indexOf("3.1")).toBe(2); + expect(sort.indexOf("3.05")).toBe(3); + expect(sort.indexOf("2")).toBe(4); + expect(sort.indexOf("1")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 2.1, 3, 3.1, 3.5, 4, 5, + ]); + }); + + test("sort removing test", async () => { + const sort = new BinarySortArray<{ + value: number; + }>( + (a, b) => { + const r = a.value - b.value; + if (Math.abs(r) <= 0.1 + Number.EPSILON) { + return 0; + } + return r; + }, + () => { + // noop + }, + () => { + // noop + } + ); + + sort.pushAndSort("2", { + value: 2, + }); + sort.pushAndSort("2.1", { + value: 2.1, + }); + sort.pushAndSort("1", { + value: 1, + }); + sort.pushAndSort("3", { + value: 3, + }); + sort.pushAndSort("3.1", { + value: 3.1, + }); + sort.pushAndSort("3.05", { + value: 3.05, + }); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("2.1")).toBe(2); + expect(sort.indexOf("3")).toBe(3); + expect(sort.indexOf("3.1")).toBe(4); + expect(sort.indexOf("3.05")).toBe(5); + expect(sort.arr.map((v) => v.value)).toStrictEqual([ + 1, 2, 2.1, 3, 3.1, 3.05, + ]); + + expect(sort.remove("2.1")).toBe(true); + expect(sort.indexOf("1")).toBe(0); + expect(sort.indexOf("2")).toBe(1); + expect(sort.indexOf("3")).toBe(2); + expect(sort.indexOf("3.1")).toBe(3); + expect(sort.indexOf("3.05")).toBe(4); + expect(sort.arr.map((v) => v.value)).toStrictEqual([1, 2, 3, 3.1, 3.05]); + + expect(sort.remove("1")).toBe(true); + expect(sort.indexOf("2")).toBe(0); + expect(sort.indexOf("3")).toBe(1); + expect(sort.indexOf("3.1")).toBe(2); + expect(sort.indexOf("3.05")).toBe(3); + expect(sort.arr.map((v) => v.value)).toStrictEqual([2, 3, 3.1, 3.05]); + + expect(sort.remove("3")).toBe(true); + expect(sort.indexOf("2")).toBe(0); + expect(sort.indexOf("3.1")).toBe(1); + expect(sort.indexOf("3.05")).toBe(2); + expect(sort.arr.map((v) => v.value)).toStrictEqual([2, 3.1, 3.05]); + + expect(sort.remove("3.05")).toBe(true); + expect(sort.indexOf("2")).toBe(0); + expect(sort.indexOf("3.1")).toBe(1); + expect(sort.arr.map((v) => v.value)).toStrictEqual([2, 3.1]); + + expect(sort.remove("2")).toBe(true); + expect(sort.indexOf("3.1")).toBe(0); + expect(sort.arr.map((v) => v.value)).toStrictEqual([3.1]); + + expect(sort.remove("3.1")).toBe(true); + expect(sort.arr.map((v) => v.value)).toStrictEqual([]); + }); +}); diff --git a/packages/extension/src/stores/huge-queries/sort.ts b/packages/extension/src/stores/huge-queries/sort.ts new file mode 100644 index 0000000000..8e94276269 --- /dev/null +++ b/packages/extension/src/stores/huge-queries/sort.ts @@ -0,0 +1,236 @@ +import { + action, + makeObservable, + observable, + onBecomeObserved, + onBecomeUnobserved, +} from "mobx"; + +// 대충 만들었으니 꼭 필요하지 않으면 쓰지 말 것... +// NOTE: T는 무조건 object여야하고 어떤 클래스의 instance면 안된다. +export class BinarySortArray { + static readonly SymbolKey = Symbol("__key"); + + @observable.ref + protected _arr: (T & { + [BinarySortArray.SymbolKey]: string; + })[] = []; + protected readonly indexForKey = new Map(); + protected readonly compareFn: (a: T, b: T) => number; + + constructor( + compareFn: (a: T, b: T) => number, + onObserved: () => void, + onUnobserved: () => void + ) { + this.compareFn = compareFn; + + makeObservable(this); + + let i = 0; + onBecomeObserved(this, "_arr", () => { + i++; + if (i === 1) { + onObserved(); + } + }); + onBecomeUnobserved(this, "_arr", () => { + i--; + if (i === 0) { + onUnobserved(); + } + }); + } + + @action + pushAndSort(key: string, value: T): boolean { + const prevIndex = this.indexForKey.get(key); + + const v = { + ...value, + [BinarySortArray.SymbolKey]: key, + }; + + if (this._arr.length === 0) { + this._arr.push(v); + this.indexForKey.set(key, 0); + // Update reference + this._arr = this._arr.slice(); + return false; + } + + if (prevIndex != null && prevIndex >= 0) { + // 이미 존재했을때 + // 위치를 수정할 필요가 없으면 값만 바꾼다. + let b = false; + if (prevIndex > 0) { + const prev = this._arr[prevIndex - 1]; + b = this.compareFn(prev, value) <= 0; + } + if (b || prevIndex === 0) { + if (prevIndex < this._arr.length - 1) { + const next = this._arr[prevIndex + 1]; + b = this.compareFn(value, next) <= 0; + } + } + + if (b) { + this._arr[prevIndex] = v; + // Update reference + this._arr = this._arr.slice(); + return true; + } + } + + // Do binary insertion sort + let left = 0; + let right = this._arr.length - 1; + let mid = 0; + while (left <= right) { + mid = Math.floor((left + right) / 2); + const el = this._arr[mid]; + const compareRes = this.compareFn(el, value); + if (compareRes === 0) { + if (prevIndex != null && prevIndex >= 0) { + const elKey = el[BinarySortArray.SymbolKey]; + const elIndex = this.indexForKey.get(elKey)!; + const compareIndexRes = prevIndex - elIndex; + if (compareIndexRes < 0) { + left = mid + 1; + } else if (compareIndexRes > 0) { + right = mid - 1; + } else { + // Can't be happened + break; + } + } else { + left = mid + 1; + } + } else if (compareRes < 0) { + left = mid + 1; + } else { + right = mid - 1; + } + } + if (right < 0) { + mid = Math.floor((left + right) / 2); + } else { + mid = Math.ceil((left + right) / 2); + } + if (mid < 0) { + for (let i = 0; i < this._arr.length; i++) { + if (prevIndex != null && prevIndex <= i) { + break; + } + this.indexForKey.set(this._arr[i][BinarySortArray.SymbolKey], i + 1); + } + if (prevIndex != null && prevIndex >= 0) { + this._arr.splice(prevIndex, 1); + } + this._arr.unshift(v); + // Update reference + this._arr = this._arr.slice(); + this.indexForKey.set(key, 0); + } else if (mid >= this._arr.length) { + if (prevIndex != null) { + for (let i = prevIndex + 1; i < this._arr.length; i++) { + this.indexForKey.set(this._arr[i][BinarySortArray.SymbolKey], i - 1); + } + } + if (prevIndex != null && prevIndex >= 0) { + this._arr.splice(prevIndex, 1); + } + this._arr.push(v); + // Update reference + this._arr = this._arr.slice(); + this.indexForKey.set(key, this._arr.length - 1); + } else { + if (prevIndex != null && prevIndex >= 0) { + if (prevIndex < mid) { + for (let i = prevIndex + 1; i < mid; i++) { + this.indexForKey.set( + this._arr[i][BinarySortArray.SymbolKey], + i - 1 + ); + } + } else { + for (let i = mid; i < prevIndex; i++) { + this.indexForKey.set( + this._arr[i][BinarySortArray.SymbolKey], + i + 1 + ); + } + } + } else { + for (let i = mid; i < this._arr.length; i++) { + this.indexForKey.set(this._arr[i][BinarySortArray.SymbolKey], i + 1); + } + } + if (prevIndex != null && prevIndex >= 0) { + if (prevIndex < mid) { + this._arr.splice(mid, 0, v); + this._arr.splice(prevIndex, 1); + // prev가 삭제되었으므로 이 이후에 실제 설정되어야할 index는 mid - 1이다. + mid = mid - 1; + } else if (prevIndex > mid) { + this._arr.splice(prevIndex, 1); + this._arr.splice(mid, 0, v); + } else { + this._arr[prevIndex] = v; + } + } else { + this._arr.splice(mid, 0, v); + } + // Update reference + this._arr = this._arr.slice(); + this.indexForKey.set(key, mid); + } + + // 이미 존재했으면(sort이면) true, 새롭게 추가되었으면(pushAndSort이면) false + return prevIndex != null && prevIndex >= 0; + } + + @action + remove(key: string) { + const index = this.indexForKey.get(key); + if (index != null && index >= 0) { + this.indexForKey.delete(key); + for (let i = index + 1; i < this._arr.length; i++) { + this.indexForKey.set(this._arr[i][BinarySortArray.SymbolKey], i - 1); + } + this._arr.splice(index, 1); + // Update reference + this._arr = this._arr.slice(); + return true; + } + return false; + } + + indexForKeyMap(): ReadonlyMap { + return this.indexForKey; + } + + indexOf(key: string): number { + const index = this.indexForKey.get(key); + if (index == null || index < 0) { + return -1; + } + return index; + } + + get arr(): ReadonlyArray< + T & { + [BinarySortArray.SymbolKey]: string; + } + > { + return this._arr; + } + + get(key: string): T | null { + const index = this.indexForKey.get(key); + if (index == null || index < 0) { + return null; + } + return this._arr[index]; + } +}