Skip to content

Commit 9eb9f43

Browse files
authored
Merge pull request #19 from spacemeshos/tweak-events-data
Tweak GUI for events data
2 parents 316846c + 965371a commit 9eb9f43

21 files changed

+1467
-756
lines changed

src/api/getFetchAll.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ type FetchAllFn<Arg, Res> = (rpc: string, arg: Arg) => Promise<Res[]>;
99
const getFetchAll = <Arg, Res>(
1010
fn: FetchChunkFn<Arg, Res>,
1111
perPage = 100,
12-
maxCycles = 10
12+
maxCycles = Infinity
1313
): FetchAllFn<Arg, Res> => {
1414
let cycle = 1;
15-
const fetchNextChunk = async (
16-
rpc: string,
17-
arg: Arg,
18-
page: number
19-
): Promise<Res[]> => {
20-
const res = await fn(rpc, arg, perPage, page * perPage);
15+
const prevOffset: Record<string, number> = {};
16+
const fetchNextChunk = async (rpc: string, arg: Arg): Promise<Res[]> => {
17+
const key = JSON.stringify(arg);
18+
const curOffset = prevOffset[key] ?? 0;
19+
const res = await fn(rpc, arg, perPage, curOffset);
20+
prevOffset[key] = curOffset + res.length;
2121
if (res.length === 100 && cycle < maxCycles) {
2222
cycle += 1;
23-
return [...res, ...(await fetchNextChunk(rpc, arg, page + 1))];
23+
return [...res, ...(await fetchNextChunk(rpc, arg))];
2424
}
2525
return res;
2626
};
2727

28-
return (rpc: string, arg: Arg) => fetchNextChunk(rpc, arg, 0);
28+
return (rpc: string, arg: Arg) => fetchNextChunk(rpc, arg);
2929
};
3030

3131
export default getFetchAll;

src/api/requests/activations.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { z } from 'zod';
2+
3+
import fetchJSON from '../../utils/fetchJSON';
4+
import { parseResponse } from '../schemas/error';
5+
6+
// eslint-disable-next-line import/prefer-default-export
7+
export const fetchActivationsCount = (rpc: string) =>
8+
fetchJSON(`${rpc}/spacemesh.v2beta1.ActivationService/ActivationsCount`, {
9+
method: 'GET',
10+
})
11+
.then(parseResponse(z.object({ count: z.number() })))
12+
.then((res) => res.count);

src/api/requests/smesherState.ts

+86-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,97 @@
11
import fetchJSON from '../../utils/fetchJSON';
22
import { parseResponse } from '../schemas/error';
3-
import { SmesherStatesResponseSchema } from '../schemas/smesherStates';
3+
import {
4+
IdentityStateInfo,
5+
SmesherStatesResponseSchema,
6+
} from '../schemas/smesherStates';
7+
import SortOrder from '../sortOrder';
48

5-
// eslint-disable-next-line import/prefer-default-export
6-
export const fetchSmesherStates = (rpc: string, limit = 100) => {
7-
const params = new URLSearchParams({ limit: limit.toString() });
9+
const PER_PAGE = 100;
10+
11+
export const fetchSmesherStatesChunk = (
12+
rpc: string,
13+
limit = PER_PAGE,
14+
order = SortOrder.DESC,
15+
to = new Date(),
16+
from: Date | undefined = undefined
17+
) => {
18+
const params = new URLSearchParams({
19+
limit: limit.toString(),
20+
order: order.toString(),
21+
to: to.toISOString(),
22+
...(from ? { from: from.toISOString() } : {}),
23+
});
824
return fetchJSON(
925
`${rpc}/spacemesh.v2beta1.SmeshingIdentitiesService/States?${params}`,
1026
{
1127
method: 'GET',
1228
}
1329
)
1430
.then(parseResponse(SmesherStatesResponseSchema))
15-
.then((res) => res.identities);
31+
.then((res) => res.states);
32+
};
33+
34+
export type SmesherStates = IdentityStateInfo[];
35+
36+
export type SmesherStatesSetter = (states: SmesherStates) => void;
37+
export const fetchSmesherStatesWithCallback = (setter: SmesherStatesSetter) => {
38+
let isInProcess = false;
39+
// Datetime range of oldest and newest fetched states
40+
// Used to fetch all events within the specified range
41+
let fetched: [Date, Date] = [new Date(), new Date()];
42+
43+
return async (
44+
rpc: string,
45+
order = SortOrder.ASC,
46+
to = new Date(),
47+
from: Date | undefined = undefined
48+
) => {
49+
if (isInProcess) return;
50+
51+
const fetchNext = async (chunkTo: Date, chunkFrom?: Date) => {
52+
const res = await fetchSmesherStatesChunk(
53+
rpc,
54+
PER_PAGE,
55+
order,
56+
chunkTo,
57+
chunkFrom
58+
);
59+
const len = res.length;
60+
61+
const first = res[0];
62+
const last = res[len - 1];
63+
if (!first?.time) {
64+
throw new Error(
65+
`Smesher event (first) supposed to have timestamp: ${JSON.stringify(
66+
first
67+
)}`
68+
);
69+
}
70+
if (!last?.time) {
71+
throw new Error(
72+
`Smesher event (last) supposed to have timestamp: ${JSON.stringify(
73+
last
74+
)}`
75+
);
76+
}
77+
fetched =
78+
order === SortOrder.ASC
79+
? [new Date(first.time), new Date(last.time)]
80+
: [new Date(last.time), new Date(first.time)];
81+
82+
setter(res);
83+
if (len === PER_PAGE) {
84+
if (order === SortOrder.ASC) {
85+
await fetchNext(to, fetched[1]);
86+
} else {
87+
await fetchNext(fetched[0], from);
88+
}
89+
} else {
90+
isInProcess = false;
91+
}
92+
};
93+
94+
isInProcess = true;
95+
await fetchNext(to, from);
96+
};
1697
};

src/api/schemas/smesherEvents.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,18 @@ export enum EventName {
3333

3434
const BaseEventSchema = <K extends EventName>(eventName: K) =>
3535
z.object({
36+
smesher: Base64Schema,
3637
state: z.literal(eventName),
3738
publishEpoch: z.optional(z.number()),
3839
time: z.string().datetime(),
3940
});
4041

42+
type BaseEvent<K extends EventName> = ReturnType<typeof BaseEventSchema<K>>;
43+
4144
function SmesherHistoryItem<T extends z.ZodRawShape>(
4245
state: EventName,
4346
detailsSchema: z.ZodObject<T>
44-
): z.ZodObject<
45-
{
46-
state: z.ZodLiteral<EventName>;
47-
publishEpoch?: z.ZodOptional<z.ZodNumber>;
48-
time: z.ZodString;
49-
} & T
50-
> {
47+
): z.ZodObject<BaseEvent<EventName>['shape'] & T> {
5148
const baseSchema = BaseEventSchema(state);
5249
return z.object({
5350
...baseSchema.shape,

src/api/schemas/smesherStates.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,6 @@ export const SmesherHistoryItemSchema = z.discriminatedUnion('state', [
2323

2424
export type IdentityStateInfo = z.infer<typeof SmesherHistoryItemSchema>;
2525

26-
export const SmesherIdentitiesSchema = z.record(
27-
z.object({
28-
history: z.array(SmesherHistoryItemSchema),
29-
})
30-
);
31-
32-
export type SmesherIdentities = z.infer<typeof SmesherIdentitiesSchema>;
33-
3426
export const SmesherStatesResponseSchema = z.object({
35-
identities: SmesherIdentitiesSchema,
27+
states: z.array(SmesherHistoryItemSchema),
3628
});

src/api/sortOrder.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
enum SortOrder {
2+
ASC = 0,
3+
DESC = 1,
4+
}
5+
6+
export default SortOrder;
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import humanizeDuration from 'humanize-duration';
2+
3+
import { Code, Table, TableContainer } from '@chakra-ui/react';
4+
5+
import { Network } from '../../types/networks';
6+
import { SECOND } from '../../utils/constants';
7+
import { formatTimestamp } from '../../utils/datetime';
8+
import TableContents from '../basic/TableContents';
9+
10+
function NetworkInfo({ data }: { data: Network }): JSX.Element {
11+
return (
12+
<TableContainer>
13+
<Table size="sm" variant="unstyled">
14+
<TableContents
15+
tableKey="netInfo"
16+
tdProps={{
17+
_first: { pl: 0, w: '30%' },
18+
_last: { pr: 0 },
19+
}}
20+
data={[
21+
['Genesis Time', formatTimestamp(data.genesisTime || 0)],
22+
[
23+
'Genesis ID',
24+
<Code display="inline" wordBreak="break-all" whiteSpace="normal">
25+
{data.genesisId}
26+
</Code>,
27+
],
28+
['Layer duration', humanizeDuration(data.layerDuration * SECOND)],
29+
['Layers per epoch', data.layersPerEpoch],
30+
['Effective genesis', data.effectiveGenesisLayer],
31+
]}
32+
/>
33+
</Table>
34+
</TableContainer>
35+
);
36+
}
37+
38+
export default NetworkInfo;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Table, TableContainer } from '@chakra-ui/react';
2+
3+
import { Activations, NodeStatus } from '../../types/networks';
4+
import TableContents from '../basic/TableContents';
5+
6+
function NodeStatusInfo({
7+
node,
8+
activations,
9+
}: {
10+
node: NodeStatus;
11+
activations: Activations | null;
12+
}): JSX.Element {
13+
return (
14+
<TableContainer>
15+
<Table size="sm" variant="unstyled">
16+
<TableContents
17+
tableKey="netInfo"
18+
tdProps={{
19+
_first: { pl: 0, w: '30%' },
20+
_last: { pr: 0 },
21+
}}
22+
data={[
23+
['Sync status', node.isSynced ? 'Synced' : 'Not synced'],
24+
[
25+
'Processed layer',
26+
// eslint-disable-next-line max-len
27+
`${node.processedLayer} / ${node.currentLayer}`,
28+
],
29+
['Applied layer', node.appliedLayer],
30+
['Latest layer', node.latestLayer],
31+
['Connected peers', node.connectedPeers],
32+
[
33+
'Activations',
34+
activations === null
35+
? 'Loading...'
36+
: activations.count.toString(),
37+
],
38+
]}
39+
/>
40+
</Table>
41+
</TableContainer>
42+
);
43+
}
44+
45+
export default NodeStatusInfo;
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Text } from '@chakra-ui/react';
2+
3+
function OptionalError({
4+
store,
5+
prefix = '',
6+
}: {
7+
store: { error: Error | null };
8+
prefix?: string;
9+
}) {
10+
if (!store.error) return null;
11+
12+
return (
13+
<Text color="red.500">
14+
{prefix}
15+
{store.error.message}
16+
</Text>
17+
);
18+
}
19+
20+
export default OptionalError;

0 commit comments

Comments
 (0)