-
Notifications
You must be signed in to change notification settings - Fork 5
feat: introduce new API connection management #1981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AMIRKHANEF
wants to merge
18
commits into
PolkaGate:main
Choose a base branch
from
AMIRKHANEF:newUseApi
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+339
−421
Open
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
beba2eb
feat: introduce new API connection management
AMIRKHANEF 7d4f409
chore: resolve ci build issue
AMIRKHANEF 8f4c460
chore: resolve code rabbit comments
AMIRKHANEF fb32b75
chore: remove unused
Nick-1979 d8d8251
Merge branch 'main' into pr/1981
Nick-1979 bfd4474
Merge branch 'main' into pr/1981
Nick-1979 49ee107
chore: correct naming
Nick-1979 a708d04
refactor: simplify code
Nick-1979 adcf3d0
docs: clear logs
Nick-1979 92bdeae
chore: clear requested queue when resolving pending connections
AMIRKHANEF 0339ea1
fix: remove empty entries
Nick-1979 9198e64
refactor: resolve connection issue for selected endpoint and handle p…
AMIRKHANEF 9b1f11f
refactor: simplify code
Nick-1979 eac95bc
chore: resolve connection to light client issue
AMIRKHANEF 61a4cc8
Merge branch 'newUseApi' of https://github.com/AMIRKHANEF/polkagate-e…
AMIRKHANEF 4b5d121
chore: correct typo
Nick-1979 f6d82a1
Merge branch 'newUseApi' of https://github.com/AMIRKHANEF/polkagate-e…
Nick-1979 05e803a
chore: remove unused statement and resolve auto mode pending connection
AMIRKHANEF File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,278 +1,26 @@ | ||
| // Copyright 2019-2025 @polkadot/extension-polkagate authors & contributors | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { useCallback, useContext, useEffect, useReducer } from 'react'; | ||
| import type { ApiPromise } from '@polkadot/api'; | ||
|
|
||
| import { ApiPromise, WsProvider } from '@polkadot/api'; | ||
| import { useContext, useEffect, useState } from 'react'; | ||
|
|
||
| import EndpointManager from '../class/endpointManager2'; | ||
| import { APIContext } from '../components'; | ||
| import { fastestConnection } from '../util'; | ||
| import LCConnector from '../util/api/lightClient-connect'; | ||
| import { AUTO_MODE } from '../util/constants'; | ||
| import useEndpoint from './useEndpoint'; | ||
| import { useEndpoints } from './useEndpoints'; | ||
| import { useEndpoints } from '.'; | ||
|
|
||
| // Define types for API state and actions | ||
| interface ApiState { | ||
| api: ApiPromise | undefined; // The API object, initially undefined | ||
| isLoading: boolean; // Whether the API connection is in the loading state | ||
| error: Error | null; // Any error that occurs during the API connection process | ||
| } | ||
|
|
||
| // Reducer function to manage API state | ||
| type ApiAction = | ||
| | { type: 'SET_API'; payload: ApiPromise } | ||
| | { type: 'SET_LOADING'; payload: boolean } | ||
| | { type: 'SET_ERROR'; payload: Error }; | ||
|
|
||
| const apiReducer = (state: ApiState, action: ApiAction): ApiState => { | ||
| const { payload, type } = action; | ||
|
|
||
| switch (type) { | ||
| case 'SET_API': | ||
| return { ...state, api: payload, error: null, isLoading: false }; | ||
| case 'SET_LOADING': | ||
| return { ...state, isLoading: payload }; | ||
| case 'SET_ERROR': | ||
| return { ...state, error: payload, isLoading: false }; | ||
| default: | ||
| return state; | ||
| } | ||
| }; | ||
|
|
||
| const endpointManager = new EndpointManager(); | ||
| const isAutoMode = (e: string) => e === AUTO_MODE.value; | ||
|
|
||
| export default function useApi (genesisHash: string | null | undefined, stateApi?: ApiPromise, _endpoint?: string): ApiPromise | undefined { | ||
| const { checkForNewOne, endpoint } = useEndpoint(genesisHash, _endpoint); | ||
| const apisContext = useContext(APIContext); | ||
| export default function useApi (genesisHash: string | null | undefined): ApiPromise | undefined { | ||
| const { getApi } = useContext(APIContext); | ||
| const endpoints = useEndpoints(genesisHash); | ||
|
|
||
| const [state, dispatch] = useReducer(apiReducer, { | ||
| api: stateApi, | ||
| error: null, | ||
| isLoading: false | ||
| }); | ||
|
|
||
| // This function is called exclusively in auto mode to update the account's "auto mode" endpoint | ||
| // with the fastest endpoint available. | ||
| const updateEndpoint = useCallback((chainKey: string, selectedEndpoint: string, cbFunction?: () => void) => { | ||
| try { | ||
| const newEndpoint = { | ||
| checkForNewOne: false, | ||
| endpoint: selectedEndpoint, | ||
| isAuto: true, | ||
| timestamp: Date.now() | ||
| }; | ||
|
|
||
| endpointManager.set(chainKey, newEndpoint); | ||
|
|
||
| cbFunction?.(); | ||
| } catch (error) { | ||
| console.error(error); | ||
| } | ||
| }, []); | ||
|
|
||
| // Checks if there is an available API connection, then will change the address endpoint to the available API's endpoint | ||
| // Returns false if it was not successful to find an available API and true vice versa | ||
| const connectToExisted = useCallback((genesisHash: string): boolean => { | ||
| const apiList = apisContext.apis[genesisHash]; | ||
|
|
||
| if (!apiList) { | ||
| return false; | ||
| } | ||
|
|
||
| const availableApi = apiList.find(({ api }) => api?.isConnected); | ||
|
|
||
| if (!availableApi?.api) { | ||
| return false; | ||
| } | ||
|
|
||
| dispatch({ payload: availableApi.api, type: 'SET_API' }); | ||
| updateEndpoint(genesisHash, availableApi.endpoint); | ||
|
|
||
| console.log('Successfully connected to existing API for genesis hash:', genesisHash); | ||
|
|
||
| return true; | ||
| }, [apisContext.apis, updateEndpoint]); | ||
|
|
||
| // Handles a new API connection and updates the context with the new API | ||
| const handleNewApi = useCallback((api: ApiPromise, endpoint: string, onAutoMode?: boolean) => { | ||
| dispatch({ payload: api, type: 'SET_API' }); | ||
|
|
||
| const genesisHash = String(api.genesisHash.toHex()); | ||
| let toSaveApi = apisContext.apis[genesisHash] ?? []; | ||
|
|
||
| // Remove any existing API with the same endpoint | ||
| // it happens when the API is requested and not connected yet | ||
| toSaveApi = toSaveApi.filter((sApi) => sApi.endpoint !== endpoint); | ||
|
|
||
| // If in auto mode, remove any auto mode endpoint | ||
| if (onAutoMode) { | ||
| toSaveApi = toSaveApi.filter((sApi) => !isAutoMode(sApi.endpoint)); | ||
| } | ||
|
|
||
| // Add the new API entry | ||
| toSaveApi.push({ | ||
| api, | ||
| endpoint, | ||
| isRequested: false | ||
| }); | ||
|
|
||
| apisContext.apis[genesisHash] = toSaveApi; | ||
| apisContext.setIt({ ...apisContext.apis }); | ||
| }, [apisContext]); | ||
|
|
||
| // Connects to a specific WebSocket endpoint and creates a new API instance | ||
| // when it is not on Auto Mode | ||
| const connectToEndpoint = useCallback(async (endpointToConnect: string) => { | ||
| try { | ||
| dispatch({ payload: true, type: 'SET_LOADING' }); | ||
| const wsProvider = new WsProvider(endpointToConnect); | ||
| const newApi = await ApiPromise.create({ provider: wsProvider }); | ||
|
|
||
| handleNewApi(newApi, endpointToConnect); | ||
| } catch (error) { | ||
| dispatch({ payload: error as Error, type: 'SET_ERROR' }); | ||
| } finally { | ||
| dispatch({ payload: false, type: 'SET_LOADING' }); | ||
| } | ||
| }, [handleNewApi]); | ||
|
|
||
| // Handles auto mode by finding the fastest endpoint and connecting to it | ||
| const handleAutoMode = useCallback(async (genesisHash: string, findNewEndpoint: boolean) => { | ||
| const apisForGenesis = apisContext.apis[genesisHash]; | ||
|
|
||
| const autoModeExists = apisForGenesis?.some(({ endpoint }) => isAutoMode(endpoint)); | ||
|
|
||
| if (autoModeExists) { | ||
| return; | ||
| } | ||
|
|
||
| const result = !findNewEndpoint && connectToExisted(genesisHash); | ||
|
|
||
| if (result) { | ||
| return; | ||
| } | ||
|
|
||
| const wssEndpoints = endpoints.filter(({ value }) => String(value).startsWith('wss')); // to filter possible light client | ||
|
|
||
| dispatch({ payload: true, type: 'SET_LOADING' }); | ||
|
|
||
| // Finds the fastest available endpoint and connects to it | ||
| const { api, selectedEndpoint } = await fastestConnection(wssEndpoints); | ||
| const [api, setApi] = useState<ApiPromise | undefined>(undefined); | ||
|
|
||
| if (!api || !selectedEndpoint) { | ||
| return; | ||
| } | ||
|
|
||
| updateEndpoint(genesisHash, selectedEndpoint, () => handleNewApi(api, selectedEndpoint, true)); | ||
| }, [apisContext.apis, connectToExisted, endpoints, handleNewApi, updateEndpoint]); | ||
|
|
||
| const addApiRequest = useCallback((endpointToRequest: string, genesisHash: string) => { | ||
| const toSaveApi = apisContext.apis[genesisHash] ?? []; | ||
|
|
||
| toSaveApi.push({ endpoint: endpointToRequest, isRequested: true }); | ||
|
|
||
| apisContext.apis[genesisHash] = toSaveApi; | ||
| apisContext.setIt({ ...apisContext.apis }); | ||
| }, [apisContext]); | ||
|
|
||
| // check api in the context | ||
| const isInContext = useCallback((endpoint: string, genesisHash: string) => { | ||
| // Check if there is a saved API that is already connected | ||
| const savedApi = apisContext?.apis[genesisHash]?.find((sApi) => sApi.endpoint === endpoint); | ||
|
|
||
| // If the API is already being requested, skip the connection process | ||
| if (savedApi?.isRequested) { | ||
| return true; | ||
| } | ||
|
|
||
| if (savedApi?.api?.isConnected) { | ||
| dispatch({ payload: savedApi.api, type: 'SET_API' }); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }, [apisContext?.apis]); | ||
|
|
||
| // Handles connection request to a manual endpoint | ||
| const handleApiWithChain = useCallback((manualEndpoint: string, genesisHash: string) => { | ||
| if (isInContext(manualEndpoint, genesisHash)) { | ||
| return; | ||
| } | ||
|
|
||
| addApiRequest(manualEndpoint, genesisHash); | ||
|
|
||
| connectToEndpoint(manualEndpoint).catch(console.error); | ||
| }, [addApiRequest, connectToEndpoint, isInContext]); | ||
|
|
||
| useEffect(() => { | ||
| // if _endpoint & genesisHash are available means useApiWithChain2 is trying to create a new connection! | ||
| if (_endpoint && genesisHash) { | ||
| handleApiWithChain(_endpoint, genesisHash); | ||
| } | ||
| }, [_endpoint, genesisHash, handleApiWithChain]); | ||
|
|
||
| // Manages the API connection when the endpoint, or genesis hash changes | ||
| useEffect(() => { | ||
| // @ts-expect-error to bypass access to private prop | ||
| if (!genesisHash || !endpoint || state.isLoading || state?.api?._options?.provider?.endpoint === endpoint) { | ||
| return; | ||
| } | ||
|
|
||
| // Validate the endpoint format (it should start with 'wss', 'light', or be in auto mode) | ||
| if (!endpoint.startsWith('wss') && !endpoint.startsWith('light') && !isAutoMode(endpoint)) { | ||
| console.log('📌 📌 Unsupported endpoint detected 📌 📌 ', endpoint); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| // To address the delay issue when setting the endpoint in this hook, | ||
| // we manually compare the endpoint obtained from `useEndpoint` (local state) | ||
| // and the endpoint stored in `EndpointManager`. | ||
| // If they are not equal, it means the state has not been updated yet, | ||
| // so we log a message and return early to prevent further processing. | ||
| const endpointFromTheManager = endpointManager.get(genesisHash)?.endpoint; // Endpoint stored in the manager | ||
|
|
||
| // Check if the two endpoints are not synchronized | ||
| if (endpoint !== endpointFromTheManager) { | ||
| // console.log('📌 📌 Not updated yet! The endpoint in the manager is still different from the local one.'); | ||
|
|
||
| // Exit early to avoid further execution until the endpoints are in sync | ||
| if (!genesisHash || !endpoints) { | ||
| return; | ||
| } | ||
|
|
||
| // To provide api from context | ||
| if (isInContext(endpoint, genesisHash)) { | ||
| return; | ||
| } | ||
|
|
||
| // If in auto mode, check existing connections or find a new one | ||
| if (isAutoMode(endpoint)) { | ||
| handleAutoMode(genesisHash, !!checkForNewOne).catch(console.error); | ||
| } | ||
|
|
||
| // Connect to a WebSocket endpoint if provided | ||
| if (endpoint.startsWith('wss')) { | ||
| connectToEndpoint(endpoint).catch(console.error); | ||
| } | ||
|
|
||
| // Connect to a light client endpoint if provided | ||
| if (endpoint.startsWith('light')) { | ||
| LCConnector(endpoint).then((LCapi) => { | ||
| handleNewApi(LCapi, endpoint); | ||
| console.log('🖌️ light client connected', String(LCapi.genesisHash.toHex())); | ||
| }).catch((err) => { | ||
| console.error('📌 light client failed:', err); | ||
| }); | ||
| } | ||
|
|
||
| addApiRequest(endpoint, genesisHash); | ||
| // @ts-expect-error to bypass access to private prop | ||
| }, [addApiRequest, genesisHash, checkForNewOne, connectToEndpoint, endpoint, handleAutoMode, handleNewApi, isInContext, state?.api?._options?.provider?.endpoint, state.isLoading]); | ||
| getApi(genesisHash, endpoints)?.then(setApi).catch(console.error); | ||
| }, [endpoints, genesisHash, getApi]); | ||
|
|
||
| return state.api; | ||
| return api; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.