Skip to content

Commit

Permalink
minifront retry status stream
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Feb 25, 2025
1 parent 138a833 commit 6205c07
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 166 deletions.
20 changes: 14 additions & 6 deletions apps/minifront/src/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { CondensedBlockSyncStatus } from '@penumbra-zone/ui-deprecated/components/ui/block-sync-status';
import { IncompatibleBrowserBanner } from '@penumbra-zone/ui-deprecated/components/ui/incompatible-browser-banner';
import { TestnetBanner } from '@penumbra-zone/ui-deprecated/components/ui/testnet-banner';
import { useEffect, useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { getChainId } from '../../fetchers/chain-id';
import { useStatus } from '../../state/status';
import { statusStreamStateSelector, useInitialStatus, useStatus } from '../../state/status';
import { PagePath } from '../metadata/paths';
import { MenuBar } from './menu/menu';
import { useStore } from '../../state';

export const Header = () => {
const { data, error } = useStatus();
const [chainId, setChainId] = useState<string | undefined>();
const initialStatus = useInitialStatus();
const status = useStatus();
const { error: streamError } = useStore(statusStreamStateSelector);

const syncData = useMemo(
() => ({ ...initialStatus.data, ...status.data }),
[initialStatus.data, status.data],
);

useEffect(() => {
void getChainId().then(id => setChainId(id));
Expand All @@ -21,9 +29,9 @@ export const Header = () => {
<IncompatibleBrowserBanner />
<TestnetBanner chainId={chainId} />
<CondensedBlockSyncStatus
fullSyncHeight={data?.fullSyncHeight}
latestKnownBlockHeight={data?.latestKnownBlockHeight}
error={error}
fullSyncHeight={syncData.fullSyncHeight}
latestKnownBlockHeight={syncData.latestKnownBlockHeight}
error={streamError}
/>
<div className='flex w-full flex-col items-center justify-between px-6 md:h-[82px] md:flex-row md:gap-12 md:px-12'>
<HeaderLogo />
Expand Down
103 changes: 73 additions & 30 deletions apps/minifront/src/components/syncing-dialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,98 @@
import { Dialog } from '@penumbra-zone/ui-deprecated/Dialog';
import { statusSelector, useStatus } from '../../state/status';
import { statusStreamStateSelector, useInitialStatus, useStatus } from '../../state/status';
import { SyncAnimation } from './sync-animation';
import { Text } from '@penumbra-zone/ui-deprecated/Text';
import { useEffect, useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import { useSyncProgress } from '@penumbra-zone/ui-deprecated/components/ui/block-sync-status';
import { useStore } from '../../state';

export const SyncingDialog = () => {
const status = useStatus({
select: statusSelector,
});
const initialStatus = useInitialStatus();
const status = useStatus();
const { error: streamError } = useStore(statusStreamStateSelector);

const [isOpen, setIsOpen] = useState(false);
const [didClose, setDidClose] = useState(false);
const [dialogText, setDialogText] = useState<string | undefined>();

const syncData = useMemo(
() => ({ ...initialStatus.data, ...status.data }),
[initialStatus.data, status.data],
);

const isSynced = useMemo(
() =>
syncData.fullSyncHeight &&
syncData.latestKnownBlockHeight &&
syncData.fullSyncHeight >= syncData.latestKnownBlockHeight,
[syncData],
);

useEffect(() => {
if (status?.isCatchingUp) {
setIsOpen(true);
} else {
setIsOpen(false);
if (streamError) {
setDialogText('Retrying...');
} else if (!initialStatus.data) {
setDialogText('Getting local block height...');
} else if (!status.data) {
setDialogText('Getting remote block height...');
} else if (!isSynced) {
setDialogText('Syncing...');
}
}, [status?.isCatchingUp]);

const shouldShow = !isSynced || !!streamError;
setIsOpen(shouldShow && !didClose);
}, [didClose, initialStatus.data, status.data, isSynced, streamError]);

return (
<Dialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
<Dialog isOpen={isOpen} onClose={() => setDidClose(true)}>
<Dialog.Content title='Your client is syncing...' zIndex={9999}>
<SyncAnimation />

<div className='text-center'>
<Text body as='p'>
Decrypting blocks to update your local state
<Text body>
{dialogText}
{streamError ? (
<Text technical color={theme => theme.caution.main} as='div'>
{streamError instanceof Error ? streamError.message : String(streamError)}
</Text>
) : undefined}
{syncData.fullSyncHeight && syncData.latestKnownBlockHeight ? (
<>
<BlockProgress
fullSyncHeight={syncData.fullSyncHeight}
latestKnownBlockHeight={syncData.latestKnownBlockHeight}
/>
<RemainingTime
fullSyncHeight={syncData.fullSyncHeight}
latestKnownBlockHeight={syncData.latestKnownBlockHeight}
/>
</>
) : undefined}
</Text>
<Text small as='p'>
You can click away, but your data <i>may</i> not be current
</Text>
<div className='mt-6'>
{!!status?.isCatchingUp && (
<Text technical>
{!!status.percentSynced && `${status.percentSynced} Synced – `} Block{' '}
{status.fullSyncHeight.toString()}{' '}
{!!status.latestKnownBlockHeight &&
`of ${status.latestKnownBlockHeight.toString()}`}
</Text>
)}
</div>
{!!status?.isCatchingUp && status.latestKnownBlockHeight ? (
<RemainingTime
fullSyncHeight={status.fullSyncHeight}
latestKnownBlockHeight={status.latestKnownBlockHeight}
/>
) : null}
</div>
</Dialog.Content>
</Dialog>
);
};

const BlockProgress = ({
fullSyncHeight,
latestKnownBlockHeight,
}: {
fullSyncHeight: bigint;
latestKnownBlockHeight: bigint;
}) => {
const formattedBlockProgress = `Block ${fullSyncHeight} of ${latestKnownBlockHeight}`;
return (
<Text technical as='div'>
{formattedBlockProgress}
</Text>
);
};

const RemainingTime = ({
fullSyncHeight,
latestKnownBlockHeight,
Expand All @@ -62,5 +101,9 @@ const RemainingTime = ({
latestKnownBlockHeight: bigint;
}) => {
const { formattedTimeRemaining } = useSyncProgress(fullSyncHeight, latestKnownBlockHeight);
return <Text technical>(Estimated time remaining: {formattedTimeRemaining})</Text>;
return (
<Text technical as='div'>
Estimated time remaining: {formattedTimeRemaining}
</Text>
);
};
39 changes: 21 additions & 18 deletions apps/minifront/src/components/v2/header/status-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,46 @@ import { Button } from '@penumbra-zone/ui-deprecated/Button';
import { Density } from '@penumbra-zone/ui-deprecated/Density';
import { Pill } from '@penumbra-zone/ui-deprecated/Pill';
import { Text } from '@penumbra-zone/ui-deprecated/Text';
import { statusSelector, useStatus } from '../../../state/status.ts';
import {
statusStreamStateSelector,
syncPercentSelector,
useStatus,
} from '../../../state/status.ts';
import { useMemo } from 'react';
import { useStore } from '../../../state';

export const StatusPopover = () => {
const status = useStatus({
select: statusSelector,
});
const sync = useStatus({ select: syncPercentSelector });
const { error: streamError } = useStore(statusStreamStateSelector);

// a ReactNode displaying the sync status in form of a pill
const pill = useMemo(() => {
// isCatchingUp is undefined when the status is not yet loaded
if (status?.isCatchingUp === undefined) {
if (sync?.percentSyncedNumber === undefined) {
return null;
}

if (status.error) {
if (streamError) {
return <Pill context='technical-destructive'>Block Sync Error</Pill>;
}

if (status.percentSyncedNumber === 1) {
if (sync.percentSyncedNumber === 1) {
return <Pill context='technical-success'>Blocks Synced</Pill>;
}

return <Pill context='technical-caution'>Block Syncing</Pill>;
}, [status]);
}, [sync, streamError]);

const popoverContext = useMemo<PopoverContext>(() => {
if (status?.isCatchingUp === undefined) {
if (sync?.percentSyncedNumber === undefined) {
return 'default';
} else if (status.error) {
} else if (streamError) {
return 'error';
} else if (status.percentSyncedNumber === 1) {
} else if (sync.percentSyncedNumber === 1) {
return 'success';
} else {
return 'caution';
}
}, [status]);
}, [sync, streamError]);

return (
<Popover>
Expand All @@ -49,21 +52,21 @@ export const StatusPopover = () => {
Status
</Button>
</Popover.Trigger>
{status?.isCatchingUp !== undefined && (
{sync?.percentSyncedNumber !== undefined && (
<Popover.Content context={popoverContext} align='end' side='bottom'>
<Density compact>
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2'>
<Text technical>Status</Text>
{pill}
{!!status.error && String(status.error)}
{!!streamError && String(streamError)}
</div>
<div className='flex flex-col gap-2'>
<Text technical>Block Height</Text>
<Pill context='technical-default'>
{status.latestKnownBlockHeight !== status.fullSyncHeight
? `${status.fullSyncHeight} of ${status.latestKnownBlockHeight}`
: `${status.latestKnownBlockHeight}`}
{sync.data?.latestKnownBlockHeight !== sync.data?.fullSyncHeight
? `${sync.data?.fullSyncHeight} of ${sync.data?.latestKnownBlockHeight}`
: `${sync.data?.latestKnownBlockHeight}`}
</Pill>
</div>
</div>
Expand Down
22 changes: 11 additions & 11 deletions apps/minifront/src/components/v2/header/sync-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { statusSelector, useStatus } from '../../../state/status.ts';
import {
statusStreamStateSelector,
syncPercentSelector,
useStatus,
} from '../../../state/status.ts';
import { Progress } from '@penumbra-zone/ui-deprecated/Progress';
import { useStore } from '../../../state';

export const SyncBar = () => {
const status = useStatus({
select: statusSelector,
});
const sync = useStatus({ select: syncPercentSelector });
const { error: streamError } = useStore(statusStreamStateSelector);

return (
<div className='fixed left-0 top-0 h-1 w-full'>
{status?.isCatchingUp === undefined ? (
<Progress value={0} loading error={Boolean(status?.error)} />
{sync?.percentSyncedNumber !== undefined ? (
<Progress value={sync.percentSyncedNumber} />
) : (
<Progress
value={status.percentSyncedNumber}
loading={status.isUpdating}
error={Boolean(status.error)}
/>
<Progress value={0} loading={sync?.loading} error={Boolean(streamError)} />
)}
</div>
);
Expand Down
37 changes: 15 additions & 22 deletions apps/minifront/src/fetchers/status.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import { ViewService } from '@penumbra-zone/protobuf';
import { penumbra } from '../penumbra';
import { CallOptions } from '@connectrpc/connect';
import { PlainMessage, toPlainMessage } from '@bufbuild/protobuf';
import {
StatusResponse,
StatusStreamResponse,
} from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';

const getInitialStatus = () =>
penumbra
.service(ViewService)
.status({})
.then(status => ({
fullSyncHeight: status.fullSyncHeight,
latestKnownBlockHeight: status.catchingUp ? undefined : status.fullSyncHeight,
}));

export async function* getStatusStream(): AsyncGenerator<{
fullSyncHeight?: bigint;
latestKnownBlockHeight?: bigint;
}> {
// `statusStream` sends new data to stream only when a new block is detected.
// This can take up to 5 seconds (time of new block generated).
// Therefore, we need to do a unary request to start us off.
yield await getInitialStatus();
export const getInitialStatus = async (opt?: CallOptions): Promise<PlainMessage<StatusResponse>> =>
toPlainMessage(await penumbra.service(ViewService).status({}, opt));

for await (const result of penumbra.service(ViewService).statusStream({})) {
yield {
fullSyncHeight: result.fullSyncHeight,
latestKnownBlockHeight: result.latestKnownBlockHeight,
};
export async function* getStatusStream(
opt?: CallOptions,
): AsyncGenerator<PlainMessage<StatusStreamResponse>> {
for await (const item of penumbra
.service(ViewService)
.statusStream({}, { timeoutMs: 15_000, ...opt })) {
yield toPlainMessage(item);
}
}
Loading

0 comments on commit 6205c07

Please sign in to comment.