Skip to content

Commit

Permalink
fix(idea/frontend): local node metadata storage for codes (#1593)
Browse files Browse the repository at this point in the history
nikitayutanov authored Jul 9, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 2abe4bd commit b2218d6
Showing 45 changed files with 468 additions and 373 deletions.
40 changes: 0 additions & 40 deletions idea/frontend/src/api/LocalDB.ts

This file was deleted.

2 changes: 1 addition & 1 deletion idea/frontend/src/api/code/requests.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import { CodePaginationModel } from './types';

const fetchCode = (id: string) => rpcService.callRPC<ICode>(RpcMethods.GetCode, { id });

const addCodeName = (params: { id: HexString; name: string }) => rpcService.callRPC(RpcMethods.AddCodeName, params);
const addCodeName = (id: HexString, name: string) => rpcService.callRPC(RpcMethods.AddCodeName, { id, name });

const fetchCodes = (params: PaginationModel) => rpcService.callRPC<CodePaginationModel>(RpcMethods.GetAllCodes, params);

5 changes: 1 addition & 4 deletions idea/frontend/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -5,10 +5,9 @@ import { fetchProgram, fetchPrograms, addProgramName } from './program';
import { fetchTestBalance } from './balance';
import { fetchMessage, fetchMessages } from './message';
import { fetchMetadata, addMetadata } from './metadata';
import { getLocalMetadata, PROGRAMS_LOCAL_FORAGE } from './LocalDB';
import { addState, fetchStates, fetchState } from './state';

const getNodes = (): Promise<NodeSection[]> => fetch(NODES_API_URL).then((result) => result.json());
const getNodes = () => fetch(NODES_API_URL).then((result) => result.json() as unknown as NodeSection[]);

export {
getNodes,
@@ -18,7 +17,6 @@ export {
addMetadata,
addCodeName,
fetchMetadata,
getLocalMetadata,
addState,
fetchStates,
fetchState,
@@ -27,5 +25,4 @@ export {
fetchMessage as getMessage,
fetchMessages as getMessages,
fetchTestBalance as getTestBalance,
PROGRAMS_LOCAL_FORAGE,
};
3 changes: 1 addition & 2 deletions idea/frontend/src/api/program/requests.ts
Original file line number Diff line number Diff line change
@@ -8,8 +8,7 @@ import { FetchProgramsParams, ProgramPaginationModel } from './types';

const fetchProgram = (id: string) => rpcService.callRPC<IProgram>(RpcMethods.GetProgram, { id });

const addProgramName = (params: { id: HexString; name: string }) =>
rpcService.callRPC(RpcMethods.AddProgramName, params);
const addProgramName = (id: HexString, name: string) => rpcService.callRPC(RpcMethods.AddProgramName, { id, name });

const fetchPrograms = (params: FetchProgramsParams) =>
rpcService.callRPC<ProgramPaginationModel>(RpcMethods.GetAllPrograms, params);
3 changes: 2 additions & 1 deletion idea/frontend/src/features/code/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useWasmFileHandler } from './use-wasm-file-handler';
import { useWasmFile } from './use-wasm-file';
import { useCode } from './use-code';

export { useWasmFileHandler, useWasmFile };
export { useWasmFileHandler, useWasmFile, useCode };
17 changes: 17 additions & 0 deletions idea/frontend/src/features/code/hooks/use-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HexString } from '@gear-js/api';
import { useQuery } from '@tanstack/react-query';

import { getCode } from '@/api';
import { useChain } from '@/hooks';

function useCode(id: HexString) {
const { isDevChain } = useChain();

return useQuery({
queryKey: ['code', id],
queryFn: async () => (await getCode(id)).result,
enabled: !isDevChain,
});
}

export { useCode };
4 changes: 2 additions & 2 deletions idea/frontend/src/features/code/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CodeTable } from './ui';
import { useWasmFileHandler, useWasmFile } from './hooks';
import { useWasmFileHandler, useWasmFile, useCode } from './hooks';

export { CodeTable, useWasmFileHandler, useWasmFile };
export { CodeTable, useWasmFileHandler, useWasmFile, useCode };
15 changes: 11 additions & 4 deletions idea/frontend/src/features/code/ui/code-table/code-table.tsx
Original file line number Diff line number Diff line change
@@ -6,9 +6,10 @@ import TablePlaceholderSVG from '@/shared/assets/images/placeholders/table.svg?r
import { absoluteRoutes } from '@/shared/config';
import { IdBlock } from '@/shared/ui/idBlock';
import { Table, TableRow } from '@/shared/ui/table';
import { LocalCode } from '@/features/local-indexer/types';

type Props = {
code: ICode | undefined;
code: ICode | LocalCode | undefined;
isCodeReady: boolean;
};

@@ -19,9 +20,15 @@ const CodeTable = ({ code, isCodeReady }: Props) =>
<IdBlock id={code.id} size="big" />
</TableRow>

<TableRow name="Block hash">
<IdBlock id={code.blockHash} to={generatePath(absoluteRoutes.block, { blockId: code.blockHash })} size="big" />
</TableRow>
{'blockHash' in code && (
<TableRow name="Block hash">
<IdBlock
id={code.blockHash}
to={generatePath(absoluteRoutes.block, { blockId: code.blockHash })}
size="big"
/>
</TableRow>
)}
</Table>
) : (
<ContentLoader text="There is no program" isEmpty={isCodeReady && !code}>
53 changes: 53 additions & 0 deletions idea/frontend/src/features/local-indexer/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { HexString, ProgramMetadata } from '@gear-js/api';
import localForage from 'localforage';

import { IMeta } from '@/entities/metadata';
import { isState } from '@/features/metadata';

import { METADATA_LOCAL_FORAGE, PROGRAMS_LOCAL_FORAGE } from './consts';
import { DBProgram } from './types';

const getLocalEntity =
<T>(db: typeof localForage, type: string) =>
async (id: string) => {
const result = await db.getItem<T>(id);

if (!result) throw new Error(`${type} not found`);

return { result };
};

const getLocalProgram = getLocalEntity<DBProgram>(PROGRAMS_LOCAL_FORAGE, 'Program');
const getLocalMetadata = getLocalEntity<IMeta>(METADATA_LOCAL_FORAGE, 'Metadata');

const addLocalProgram = (program: DBProgram) => PROGRAMS_LOCAL_FORAGE.setItem(program.id, program);

const addLocalProgramName = async (id: HexString, name: string) => {
const { result } = await getLocalProgram(id);

return PROGRAMS_LOCAL_FORAGE.setItem(id, { ...result, name });
};

const changeProgramStateStatus = async (metahash: HexString, metaHex: HexString) => {
let program: DBProgram | undefined;

// not an efficient way, probably would be better to get program by index.
// however, it'd require different library like idb.js
await PROGRAMS_LOCAL_FORAGE.iterate<DBProgram, unknown>((value) => {
if (value.metahash !== metahash) return;

program = value;
});

if (!program) return;

const metadata = ProgramMetadata.from(metaHex);
const hasState = isState(metadata);

return PROGRAMS_LOCAL_FORAGE.setItem(program.id, { ...program, hasState });
};

const addLocalMetadata = async (hash: HexString, hex: HexString) =>
Promise.all([METADATA_LOCAL_FORAGE.setItem(hash, { hex }), changeProgramStateStatus(hash, hex)]);

export { getLocalProgram, getLocalMetadata, addLocalProgram, addLocalProgramName, addLocalMetadata };
6 changes: 6 additions & 0 deletions idea/frontend/src/features/local-indexer/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import localForage from 'localforage';

const PROGRAMS_LOCAL_FORAGE = localForage.createInstance({ name: 'programs' });
const METADATA_LOCAL_FORAGE = localForage.createInstance({ name: 'metadata' });

export { PROGRAMS_LOCAL_FORAGE, METADATA_LOCAL_FORAGE };
3 changes: 2 additions & 1 deletion idea/frontend/src/features/local-indexer/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useLocalProgram } from './use-local-program';
import { useLocalPrograms } from './use-local-programs';
import { useLocalCode } from './use-local-code';

export { useLocalProgram, useLocalPrograms };
export { useLocalCode, useLocalProgram, useLocalPrograms };
39 changes: 39 additions & 0 deletions idea/frontend/src/features/local-indexer/hooks/use-local-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { HexString } from '@gear-js/api';
import { useApi } from '@gear-js/react-hooks';
import { useQuery } from '@tanstack/react-query';

import { useChain } from '@/hooks';

function useLocalCode(id: HexString) {
const { isDevChain } = useChain();
const { api, isApiReady } = useApi();

// TODO: useMetadataHash hook or util?
const getMetadataHash = async () => {
if (!isApiReady) throw new Error('API is not initialized');

try {
return await api.code.metaHash(id);
} catch (error) {
return null;
}
};

const getCode = async () => {
if (!isApiReady) throw new Error('API is not initialized');
if (!isDevChain) throw new Error('For indexed nodes use appropriate storage request');

const name = id;
const metahash = await getMetadataHash();

return { id, name, metahash };
};

return useQuery({
queryKey: ['local-code', id],
queryFn: getCode,
enabled: isApiReady && isDevChain,
});
}

export { useLocalCode };
68 changes: 37 additions & 31 deletions idea/frontend/src/features/local-indexer/hooks/use-local-program.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,65 @@
import { ProgramMetadata } from '@gear-js/api';
import { useApi } from '@gear-js/react-hooks';
import { HexString } from '@polkadot/util/types';
import { ProgramMetadata } from '@gear-js/api';

import { PROGRAMS_LOCAL_FORAGE } from '@/api';
import { IProgram, useProgramStatus } from '@/features/program';
import { isState, useMetadata } from '@/features/metadata';
import { IMeta } from '@/entities/metadata';
import { isState } from '@/features/metadata';
import { useProgramStatus } from '@/features/program';

import { METADATA_LOCAL_FORAGE, PROGRAMS_LOCAL_FORAGE } from '../consts';
import { DBProgram } from '../types';

function useLocalProgram() {
const { api, isApiReady } = useApi();

const { getMetadata } = useMetadata();
const { getProgramStatus } = useProgramStatus();

const getChainProgram = async (id: HexString) => {
if (!isApiReady) return Promise.reject(new Error('API is not initialized'));
// TODO: useMetadataHash hook or util?
const getMetadataHash = async (id: HexString) => {
if (!isApiReady) throw new Error('API is not initialized');

const name = id;
const status = await getProgramStatus(id);

let codeId: HexString | null;
let metahash: HexString | null;
let metaHex: HexString | null | undefined;

// cuz error on terminated program
try {
codeId = await api.program.codeHash(id);
return await api.program.metaHash(id);
} catch {
codeId = null;
return null;
}
};

try {
metahash = await api.code.metaHash(codeId || id);
} catch {
metahash = null;
}
const getCodeId = async (id: HexString) => {
if (!isApiReady) throw new Error('API is not initialized');

// metadata is retrived via useMetadata, so no need to log errors here
// cuz error on terminated program
try {
metaHex = metahash ? (await getMetadata(metahash)).result.hex : undefined;
return await api.program.codeHash(id);
} catch {
metaHex = null;
return null;
}
};

// TODO: on Programs page each program can make a request to backend,
// is there a way to optimize it?
const metadata = metaHex ? ProgramMetadata.from(metaHex) : undefined;
const hasState = isState(metadata);
const getHasState = async (metahash: HexString | null) => {
if (!metahash) return false;

const localForageMetadata = metahash ? await METADATA_LOCAL_FORAGE.getItem<IMeta>(metahash) : undefined;
const metadata = localForageMetadata?.hex ? ProgramMetadata.from(localForageMetadata.hex) : undefined;

return isState(metadata);
};

const getChainProgram = async (id: HexString) => {
if (!isApiReady) return Promise.reject(new Error('API is not initialized'));

const name = id;
const status = await getProgramStatus(id);
const codeId = await getCodeId(id);
const metahash = await getMetadataHash(id);
const hasState = await getHasState(metahash);

return { id, name, status, codeId, metahash, hasState };
};

const getLocalProgram = async (id: HexString) => {
if (!isApiReady) return Promise.reject(new Error('API is not initialized'));

const localForageProgram = await PROGRAMS_LOCAL_FORAGE.getItem<IProgram>(id);
const localForageProgram = await PROGRAMS_LOCAL_FORAGE.getItem<DBProgram>(id);

const isProgramInChain = id === localForageProgram?.id;
const isProgramFromChain = api.genesisHash.toHex() === localForageProgram?.genesis;
Original file line number Diff line number Diff line change
@@ -29,7 +29,9 @@ function useLocalPrograms() {

const getSortedPrograms = (programs: (IProgram | LocalProgram)[]) =>
programs.sort(
(program, nextProgram) => Date.parse(nextProgram.timestamp || '0') - Date.parse(program.timestamp || '0'),
(program, nextProgram) =>
Date.parse('timestamp' in nextProgram ? nextProgram.timestamp : '0') -
Date.parse('timestamp' in program ? program.timestamp : '0'),
);

const getLocalPrograms = async (params: FetchProgramsParams) => {
18 changes: 14 additions & 4 deletions idea/frontend/src/features/local-indexer/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { useLocalProgram, useLocalPrograms } from './hooks';
import { LocalProgram } from './types';
import { getLocalMetadata, addLocalProgram, addLocalProgramName, addLocalMetadata } from './api';
import { useLocalProgram, useLocalPrograms, useLocalCode } from './hooks';
import { LocalProgram, LocalCode } from './types';

export { useLocalProgram, useLocalPrograms };
export type { LocalProgram };
export {
getLocalMetadata,
addLocalProgram,
addLocalProgramName,
addLocalMetadata,
useLocalProgram,
useLocalPrograms,
useLocalCode,
};

export type { LocalProgram, LocalCode };
Loading

0 comments on commit b2218d6

Please sign in to comment.