DiscoveryProvider.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import React, { useState, useEffect, useContext, createContext } from 'react';
  2. import { Message } from 'semantic-ui-react';
  3. import axios, { CancelToken } from 'axios';
  4. import { StorageProviderId } from '@joystream/types/working-group';
  5. import { Vec } from '@polkadot/types';
  6. import { Url } from '@joystream/types/discovery';
  7. import ApiContext from '@polkadot/react-api/ApiContext';
  8. import { ApiProps } from '@polkadot/react-api/types';
  9. import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
  10. import { componentName } from '@polkadot/joy-utils/react/helpers';
  11. export type BootstrapNodes = {
  12. bootstrapNodes?: Url[];
  13. };
  14. export type DiscoveryProvider = {
  15. resolveAssetEndpoint: (provider: StorageProviderId, contentId?: string, cancelToken?: CancelToken) => Promise<string>;
  16. reportUnreachable: (provider: StorageProviderId) => void;
  17. };
  18. export type DiscoveryProviderProps = {
  19. discoveryProvider: DiscoveryProvider;
  20. };
  21. // return string Url with last `/` removed
  22. function normalizeUrl (url: string | Url): string {
  23. const st: string = url.toString();
  24. if (st.endsWith('/')) {
  25. return st.substring(0, st.length - 1);
  26. }
  27. return st.toString();
  28. }
  29. type ProviderStats = {
  30. assetApiEndpoint: string;
  31. unreachableReports: number;
  32. resolvedAt: number;
  33. }
  34. function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryProvider {
  35. const stats: Map<string, ProviderStats> = new Map();
  36. const resolveAssetEndpoint = async (storageProvider: StorageProviderId, contentId?: string, cancelToken?: CancelToken) => {
  37. const providerKey = storageProvider.toString();
  38. let stat = stats.get(providerKey);
  39. if (
  40. (!stat || (stat && (Date.now() > (stat.resolvedAt + (10 * 60 * 1000))))) &&
  41. bootstrapNodes
  42. ) {
  43. for (let n = 0; n < bootstrapNodes.length; n++) {
  44. const discoveryUrl = normalizeUrl(bootstrapNodes[n]);
  45. try {
  46. // eslint-disable-next-line no-new
  47. new URL(discoveryUrl);
  48. } catch (err) {
  49. continue;
  50. }
  51. const serviceInfoQuery = `${discoveryUrl}/discover/v0/${storageProvider.toString()}`;
  52. try {
  53. console.log(`Resolving ${providerKey} using ${discoveryUrl}`);
  54. const serviceInfo = await axios.get(serviceInfoQuery, { cancelToken }) as any;
  55. if (!serviceInfo) {
  56. continue;
  57. }
  58. stats.set(providerKey, {
  59. assetApiEndpoint: normalizeUrl(JSON.parse(serviceInfo.data.serialized).asset.endpoint),
  60. unreachableReports: 0,
  61. resolvedAt: Date.now()
  62. });
  63. break;
  64. } catch (err) {
  65. console.log(err);
  66. if (axios.isCancel(err)) {
  67. throw err;
  68. }
  69. continue;
  70. }
  71. }
  72. }
  73. stat = stats.get(providerKey);
  74. console.log(stat);
  75. if (stat) {
  76. return `${stat.assetApiEndpoint}/asset/v0/${contentId || ''}`;
  77. }
  78. throw new Error('Resolving failed.');
  79. };
  80. const reportUnreachable = (provider: StorageProviderId) => {
  81. const key = provider.toString();
  82. const stat = stats.get(key);
  83. if (stat) {
  84. stat.unreachableReports = stat.unreachableReports + 1;
  85. }
  86. };
  87. return { resolveAssetEndpoint, reportUnreachable };
  88. }
  89. const DiscoveryProviderContext = createContext<DiscoveryProvider>(undefined as unknown as DiscoveryProvider);
  90. export const DiscoveryProviderProvider = (props: React.PropsWithChildren<{}>) => {
  91. const api: ApiProps = useContext(ApiContext);
  92. const [provider, setProvider] = useState<DiscoveryProvider | undefined>();
  93. const [loaded, setLoaded] = useState<boolean | undefined>();
  94. useEffect(() => {
  95. const load = async () => {
  96. if (loaded || !api) return;
  97. console.log('Discovery Provider: Loading bootstrap node from Substrate...');
  98. const bootstrapNodes = await api.api.query.discovery.bootstrapEndpoints() as Vec<Url>;
  99. setProvider(newDiscoveryProvider({ bootstrapNodes }));
  100. setLoaded(true);
  101. console.log('Discovery Provider: Initialized');
  102. };
  103. load();
  104. }, [loaded]);
  105. if (!api || !api.isApiReady) {
  106. // Substrate API is not ready yet.
  107. return null;
  108. }
  109. if (!provider) {
  110. return (
  111. <Message info className='JoyMainStatus'>
  112. <Message.Header>Initializing Content Discovery Provider</Message.Header>
  113. <div style={{ marginTop: '1rem' }}>
  114. Loading bootstrap nodes... Please wait.
  115. </div>
  116. </Message>
  117. );
  118. }
  119. return (
  120. <DiscoveryProviderContext.Provider value={provider}>
  121. {props.children}
  122. </DiscoveryProviderContext.Provider>
  123. );
  124. };
  125. export const useDiscoveryProvider = () =>
  126. useContext(DiscoveryProviderContext);
  127. export function withDiscoveryProvider (Component: React.ComponentType<DiscoveryProviderProps>) {
  128. const ResultComponent: React.FunctionComponent<{}> = (props: React.PropsWithChildren<{}>) => {
  129. const discoveryProvider = useDiscoveryProvider();
  130. if (!discoveryProvider) {
  131. return <JoyInfo title={'Please wait...'}>Loading discovery provider.</JoyInfo>;
  132. }
  133. return (
  134. <Component {...props} discoveryProvider={discoveryProvider}>
  135. {props.children}
  136. </Component>
  137. );
  138. };
  139. ResultComponent.displayName = `withDiscoveryProvider(${componentName(Component)})`;
  140. return ResultComponent;
  141. }