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
  • Loading branch information
nikitayutanov authored Jul 9, 2024
1 parent 2abe4bd commit b2218d6
Show file tree
Hide file tree
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
Expand Up @@ -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);

Expand Down
5 changes: 1 addition & 4 deletions idea/frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,7 +17,6 @@ export {
addMetadata,
addCodeName,
fetchMetadata,
getLocalMetadata,
addState,
fetchStates,
fetchState,
Expand All @@ -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
Expand Up @@ -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);
Expand Down
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
Expand Up @@ -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;
};

Expand All @@ -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}>
Expand Down
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
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.