Skip to content

Commit

Permalink
Now uses web workers to load data, improving UI performance while loa…
Browse files Browse the repository at this point in the history
…ding
  • Loading branch information
GeekLad committed Oct 9, 2024
1 parent cf7f547 commit b642eff
Show file tree
Hide file tree
Showing 17 changed files with 378 additions and 187 deletions.
Binary file modified bun.lockb
Binary file not shown.
11 changes: 9 additions & 2 deletions components/summary/filter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@ export const Filter = (props: {
filter: TransactionFilter;
filterTransactions: (filter: TransactionFilter) => any;
reset: () => any;
toggleUsd: () => any;
}) => {
const [filterOn, setFilterOn] = useState(false);

return (
<Card className="md:mb-4 sm:mb-4 md:col-span-2">
<CardBody className="md:grid grid-flow-cols grid-cols-6">
<Switch className="my-4" onClick={() => props.toggleUsd()}>
<Switch
className="my-4"
onClick={() =>
props.filterTransactions({
...props.filter,
displayUsd: !props.filter.displayUsd,
})
}
>
Display USD
</Switch>
<Switch
Expand Down
1 change: 1 addition & 0 deletions components/summary/generate-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface TransactionFilter {
hawksight: HawksightStatus;
baseTokenMints: Set<string>;
quoteTokenMints: Set<string>;
displayUsd: boolean;
}

export function generateSummary(
Expand Down
274 changes: 152 additions & 122 deletions components/summary/index.tsx
Original file line number Diff line number Diff line change
@@ -1,164 +1,194 @@
import MeteoraDlmmDb, {
MeteoraDlmmDbTransactions,
} from "@geeklad/meteora-dlmm-db/dist/meteora-dlmm-db";
import { useEffect, useState, useMemo, useCallback } from "react";
import MeteoraDownloader from "@geeklad/meteora-dlmm-db/dist/meteora-dlmm-downloader";
import { MeteoraDlmmDbTransactions } from "@geeklad/meteora-dlmm-db/dist/meteora-dlmm-db";
import { useEffect, useMemo, useState, useCallback } from "react";
import { useRouter } from "next/router";
import { MeteoraDlmmDownloaderStats } from "@geeklad/meteora-dlmm-db/dist/meteora-dlmm-downloader";

import { FullPageSpinner } from "../full-page-spinner";

import { SummaryTop } from "@/components/summary/top";
import { QuoteTokenDisplay } from "@/components/summary/quote-token-display";
import { Filter } from "@/components/summary/filter";
import {
generateSummary,
TransactionFilter,
applyFilter,
SummaryData,
} from "@/components/summary/generate-summary";
import { delay } from "@/services/util";

export const Summary = (props: {
db: MeteoraDlmmDb;
downloader: MeteoraDownloader;
}) => {
const initialTransactions = useMemo(
() => props.db.getTransactions(),
[props.db],
);
import { SummaryTop } from "@/components/summary/top";
import { DataWorkerMessage } from "@/public/workers/download-worker";

export const Summary = (props: { downloadWorker: Worker }) => {
const router = useRouter();

const [allTransactions, setAllTransactions] = useState(initialTransactions);
const [summary, setSummary] = useState(generateSummary(initialTransactions));
const [filteredSummary, setFilteredSummary] = useState(
generateSummary(initialTransactions),
const [stats, setStats] = useState<MeteoraDlmmDownloaderStats>({
downloadingComplete: false,
positionsComplete: false,
transactionDownloadCancelled: false,
fullyCancelled: false,
secondsElapsed: 0,
accountSignatureCount: 0,
oldestTransactionDate: new Date(),
positionTransactionCount: 0,
positionCount: 0,
usdPositionCount: 0,
missingUsd: 0,
});
const [allTransactions, setAllTransactions] = useState<
MeteoraDlmmDbTransactions[]
>([]);
const [summary, setSummary] = useState<SummaryData>(generateSummary([]));
const [filteredSummary, setFilteredSummary] = useState<SummaryData>(
generateSummary([]),
);
const [cancelled, setCancelled] = useState(false);
const start = useMemo(() => Date.now(), [router.query.walletAddress]);
const [initialized, setInitialized] = useState(false);
const [filter, setTransactionFilter] = useState(getDefaultFilter());
const [displayUsd, setDisplayUsd] = useState(false);

function getDefaultFilter(
transactions?: MeteoraDlmmDbTransactions[],
): TransactionFilter {
transactions = transactions ? transactions : allTransactions;

return {
startDate:
transactions.length > 0
? new Date(
Math.min(...transactions.map((tx) => tx.block_time * 1000)),
)
: new Date("11/06/2023"),
endDate:
transactions.length > 0
? new Date(
Math.max(...transactions.map((tx) => tx.block_time * 1000)),
)
: new Date(Date.now() + 1000 * 60 * 60 * 24),
positionStatus: "all",
hawksight: "include",
baseTokenMints: new Set(transactions.map((tx) => tx.base_mint)),
quoteTokenMints: new Set(transactions.map((tx) => tx.quote_mint)),
} as TransactionFilter;
}
const [duration, setDuration] = useState(0);
const [done, setDone] = useState(false);
const [cancelled, setCancelled] = useState(false);
const [filter, setFilter] = useState<
TransactionFilter | undefined
>(undefined);
const [quoteTokenDisplay, setQuoteTokenDisplay] = useState<JSX.Element[]>([]);

const getDefaultFilter = useCallback(
(
transactions: MeteoraDlmmDbTransactions[] = allTransactions,
): TransactionFilter => {
return {
startDate:
transactions.length > 0
? new Date(
Math.min(...transactions.map((tx) => tx.block_time * 1000)),
)
: new Date("11/06/2023"),
endDate:
transactions.length > 0
? new Date(
Math.max(...transactions.map((tx) => tx.block_time * 1000)),
)
: new Date(Date.now() + 1000 * 60 * 60 * 24),
positionStatus: "all",
hawksight: "include",
baseTokenMints: new Set(transactions.map((tx) => tx.base_mint)),
quoteTokenMints: new Set(transactions.map((tx) => tx.quote_mint)),
displayUsd: false,
};
},
[allTransactions],
);

const filterTransactions = useCallback(
(transactions: MeteoraDlmmDbTransactions[], filter?: TransactionFilter) => {
const transactionFilter = filter || getDefaultFilter(transactions);

const filteredTransactions = applyFilter(transactions, transactionFilter);

setFilteredSummary(generateSummary(filteredTransactions));
setTransactionFilter(transactionFilter);
(
transactions: MeteoraDlmmDbTransactions[],
updatedFilter?: TransactionFilter,
) => {
setFilter((prevFilter) => {
const newFilter = {
...(prevFilter || getDefaultFilter(transactions)),
...updatedFilter,
};

const filteredTransactions = applyFilter(transactions, newFilter);
const filteredSummary = generateSummary(filteredTransactions);

setFilteredSummary(filteredSummary);
updateQuoteTokenDisplay(filteredSummary, newFilter.displayUsd);

return updatedFilter;
});
},
[],
[getDefaultFilter],
);

const readData = useCallback(async (walletAddress: string) => {
let loopCount = 0;

while (!isDone()) {
const start = Date.now();
const latestTransactions = props.db
.getTransactions()
.filter((tx) => tx.owner_address == walletAddress);

setSummary(generateSummary(latestTransactions));
setAllTransactions(latestTransactions);
filterTransactions(latestTransactions);
const dbReadTime = Date.now() - start;
const delayMs =
loopCount < 2
? 1000
: Math.max(1.5 * dbReadTime, Math.min(5000, 3 * dbReadTime));

console.log(
`${dbReadTime}ms database read time, delaying ${delayMs}ms for next database read.`,
const updateQuoteTokenDisplay = useCallback(
(summary: SummaryData, displayUsd: boolean) => {
setQuoteTokenDisplay(
Array.from(summary.quote.values()).map((s) => (
<QuoteTokenDisplay
key={s.token.mint}
displayUsd={displayUsd}
summary={s}
/>
)),
);
await delay(delayMs);
loopCount++;
}
const finalTransactions = props.db
.getTransactions()
.filter((tx) => tx.owner_address == walletAddress);

setSummary(generateSummary(finalTransactions));
setAllTransactions(finalTransactions);
filterTransactions(finalTransactions);
}, []);

function isDone() {
return (
props.downloader.downloadComplete ||
props.downloader.stats.fullyCancelled ||
props.downloader.stats.downloadingComplete
);
}
},
[],
);

function cancel() {
props.downloader.cancel();
const cancel = useCallback(() => {
props.downloadWorker.postMessage("cancel");
setCancelled(true);
}

function resetFilters() {
filterTransactions(allTransactions, getDefaultFilter());
}
}, [props.downloadWorker]);

const resetFilters = useCallback(() => {
filterTransactions(allTransactions, undefined);
}, [allTransactions, filterTransactions, getDefaultFilter]);

const update = useCallback(
(event: MessageEvent<DataWorkerMessage>) => {
if (event.data.stats.downloadingComplete) {
setDone(true);
}
const { transactions, stats } = event.data;

if (transactions.length > 0) {
setStats(stats);
setSummary(generateSummary(transactions));
setAllTransactions(transactions);
if (!initialized) {
setInitialized(true);
filterTransactions(transactions, getDefaultFilter(transactions));
} else {
filterTransactions(transactions, filter);
}
}
},
[filterTransactions, getDefaultFilter, initialized, filter],
);

useEffect(() => {
if (router.query.walletAddress && !initialized) {
setInitialized(true);
readData(router.query.walletAddress as string);
if (router.query.walletAddress) {
props.downloadWorker.onmessage = update;

const durationHandle = setInterval(() => {
setDuration(Date.now() - start);
}, 1000);

return () => {
if (done) {
clearInterval(durationHandle);
}
};
}
}, [initialized, readData]);
}, [router.query.walletAddress, props.downloadWorker, start, done, update]);

if (!initialized) {
return <FullPageSpinner excludeLayout={true} />;
}

return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="w-full">
<div className="md:grid grid-flow-cols grid-cols-2 items-start">
<SummaryTop
cancel={() => cancel()}
cancel={cancel}
cancelled={cancelled}
data={filteredSummary}
done={isDone()}
downloader={props.downloader}
done={done}
duration={duration}
stats={stats}
/>
<Filter
allTransactions={allTransactions}
data={summary}
done={isDone()}
filter={filter}
filterTransactions={(filter) =>
filterTransactions(allTransactions, filter)
done={done}
filter={filter || getDefaultFilter()}
filterTransactions={(newFilter) =>
filterTransactions(allTransactions, newFilter)
}
reset={() => resetFilters()}
toggleUsd={() => setDisplayUsd(!displayUsd)}
reset={resetFilters}
/>
</div>
{Array.from(filteredSummary.quote.values()).map((summary) => (
<QuoteTokenDisplay
key={summary.token.mint}
displayUsd={displayUsd}
summary={summary}
/>
))}
{quoteTokenDisplay}
</div>
</section>
);
Expand Down
10 changes: 6 additions & 4 deletions components/summary/top/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import MeteoraDownloader from "@geeklad/meteora-dlmm-db/dist/meteora-dlmm-downloader";
import { MeteoraDlmmDownloaderStats } from "@geeklad/meteora-dlmm-db/dist/meteora-dlmm-downloader";

import { SummaryData } from "../generate-summary";

import { SummaryLeft } from "@/components/summary/top/left";
import { SummaryRight } from "@/components/summary/top/right";

export const SummaryTop = (props: {
duration: number;
done: boolean;
data: SummaryData;
downloader: MeteoraDownloader;
stats: MeteoraDlmmDownloaderStats;
cancel: () => any;
cancelled: boolean;
}) => {
Expand All @@ -18,7 +19,8 @@ export const SummaryTop = (props: {
<SummaryLeft
data={props.data}
done={props.done}
downloader={props.downloader}
duration={props.duration}
stats={props.stats}
/>
</div>
<div className="md:ml-4 mb-4">
Expand All @@ -27,7 +29,7 @@ export const SummaryTop = (props: {
cancelled={props.cancelled}
data={props.data}
done={props.done}
downloader={props.downloader}
stats={props.stats}
/>
</div>
</>
Expand Down
Loading

0 comments on commit b642eff

Please sign in to comment.