diff --git a/packages/common/src/services/audius-backend/eagerLoadUtils.ts b/packages/common/src/services/audius-backend/eagerLoadUtils.ts index 980b1395e6..72315de0e2 100644 --- a/packages/common/src/services/audius-backend/eagerLoadUtils.ts +++ b/packages/common/src/services/audius-backend/eagerLoadUtils.ts @@ -1,3 +1,4 @@ +import { paramsToQueryString } from '../../utils/paramsToQueryString' import { Env } from '../env' import { LocalStorage } from '../local-storage' @@ -28,3 +29,56 @@ export const getEagerDiscprov = async ( } return eagerDiscprov } + +/** + * Takes a request object provided from the audius libs API and makes the request + * using the fetch API. + */ +export const makeEagerRequest = async ( + req: any, + endpoint: string, + requiresUser = false, + localStorage: LocalStorage, + env: Env +) => { + const eagerDiscprov = await getEagerDiscprov(localStorage, env) + const discprovEndpoint = endpoint ?? eagerDiscprov + const user = await localStorage.getAudiusAccountUser() + if (!user && requiresUser) throw new Error('User required to continue') + + const headers: { [key: string]: string } = {} + if (user && user.user_id) { + headers['X-User-ID'] = user.user_id + } + + let baseUrl = `${discprovEndpoint}/${req.endpoint}` + if (req.urlParams) { + baseUrl = `${baseUrl}${req.urlParams}` + } + + let res: any + if (req?.method?.toLowerCase() === 'post') { + headers['Content-Type'] = 'application/json' + const url = `${baseUrl}?${paramsToQueryString( + req.queryParams, + discprovEndpoint === eagerDiscprov + )}` + res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(req.data) + }) + } else { + const url = `${baseUrl}?${paramsToQueryString( + req.queryParams, + discprovEndpoint === eagerDiscprov + )}` + res = await fetch(url, { + headers + }) + } + + const json = await res.json() + if (json.data) return json.data + return json +} diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 6c7eda6f8d..61f36ce90e 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -17,3 +17,4 @@ export * from './sagaHelpers' export * as hlsUtils from './hls' export * from './fileUtils' export * from './constants' +export * from './paramsToQueryString' diff --git a/packages/common/src/utils/paramsToQueryString.ts b/packages/common/src/utils/paramsToQueryString.ts new file mode 100644 index 0000000000..1a7b15c641 --- /dev/null +++ b/packages/common/src/utils/paramsToQueryString.ts @@ -0,0 +1,25 @@ +/** + * Converts a provided URL params object to a query string + */ +export const paramsToQueryString = ( + params: { [key: string]: string | string[] }, + formatWithoutArray = false +) => { + if (!params) return '' + return Object.keys(params) + .map((k) => { + if (Array.isArray(params[k])) { + return (params[k] as string[]) + .map((val: string) => + formatWithoutArray + ? `${encodeURIComponent(k)}=${encodeURIComponent(val)}` + : `${encodeURIComponent(k)}[]=${encodeURIComponent(val)}` + ) + .join('&') + } + return ( + encodeURIComponent(k) + '=' + encodeURIComponent(params[k] as string) + ) + }) + .join('&') +} diff --git a/packages/mobile/src/services/audius-api-client/apiClient.ts b/packages/mobile/src/services/audius-api-client/apiClient.ts index 6cbae72717..01638d725c 100644 --- a/packages/mobile/src/services/audius-api-client/apiClient.ts +++ b/packages/mobile/src/services/audius-api-client/apiClient.ts @@ -1,7 +1,8 @@ import { AudiusAPIClient } from '@audius/common' -import { audiusBackendInstance, audiusLibs } from '../audius-backend-instance' +import { audiusBackendInstance } from '../audius-backend-instance' import { env } from '../env' +import { audiusLibs } from '../libs' import { localStorage } from '../local-storage' import { remoteConfigInstance } from '../remote-config' diff --git a/packages/mobile/src/services/audius-backend-instance.ts b/packages/mobile/src/services/audius-backend-instance.ts index 6abaa0f961..74fa5a09e9 100644 --- a/packages/mobile/src/services/audius-backend-instance.ts +++ b/packages/mobile/src/services/audius-backend-instance.ts @@ -1,8 +1,5 @@ -import EventEmitter from 'events' - import { audiusBackend } from '@audius/common' import * as nativeLibs from '@audius/sdk/dist/native-libs' -import type { AudiusLibs } from '@audius/sdk/dist/native-libs' import AsyncStorage from '@react-native-async-storage/async-storage' import Config from 'react-native-config' import scrypt from 'react-native-scrypt' @@ -10,42 +7,18 @@ import scrypt from 'react-native-scrypt' import { track } from 'app/services/analytics' import { reportToSentry } from 'app/utils/reportToSentry' +import { withEagerOption } from './eagerLoadUtils' import { env } from './env' +import { + libsInitEventEmitter, + LIBS_INITTED_EVENT, + setLibs, + waitForLibsInit +} from './libs' import { monitoringCallbacks } from './monitoringCallbacks' import { getFeatureEnabled } from './remote-config' import { remoteConfigInstance } from './remote-config/remote-config-instance' -// TODO: declare this at the root and use actual audiusLibs type -declare global { - interface Window { - audiusLibs: any - } -} - -const libsInitEventEmitter = new EventEmitter() - -export let audiusLibs: AudiusLibs - -const LIBS_INITTED_EVENT = 'LIBS_INITTED_EVENT' - -/** - * Wait for the `LIBS_INITTED_EVENT` or pass through if there - * already exists a mounted `window.audiusLibs` object. - */ -const waitForLibsInit = async () => { - // If libs is already defined, it has already loaded & initted - // so do nothing - if (audiusLibs) return - // Add an event listener and resolve when that returns - return new Promise((resolve) => { - if (audiusLibs) { - resolve() - } else { - libsInitEventEmitter.addListener(LIBS_INITTED_EVENT, resolve) - } - }) -} - function bufferFromHexString(hexString: string) { const byteArray = hexString .match(/.{1,2}/g) @@ -121,7 +94,7 @@ export const audiusBackendInstance = audiusBackend({ monitoringCallbacks, nativeMobile: true, onLibsInit: (libs) => { - audiusLibs = libs + setLibs(libs) libsInitEventEmitter.emit(LIBS_INITTED_EVENT) }, recaptchaSiteKey: Config.RECAPTCHA_SITE_KEY, @@ -158,10 +131,6 @@ export const audiusBackendInstance = audiusBackend({ }, getLibs: async () => nativeLibs, waitForLibsInit, - withEagerOption: ({ normal }, ...args) => { - if (audiusLibs) { - return normal(audiusLibs)(...args) - } - }, + withEagerOption, disableImagePreload: true }) diff --git a/packages/mobile/src/services/eagerLoadUtils.ts b/packages/mobile/src/services/eagerLoadUtils.ts new file mode 100644 index 0000000000..3ce713bbca --- /dev/null +++ b/packages/mobile/src/services/eagerLoadUtils.ts @@ -0,0 +1,52 @@ +/** + * Utilities to assist in eager pre-fetching content from the + * protocol before libs has initialized. + */ + +import { getEagerDiscprov, makeEagerRequest } from '@audius/common' + +import { env } from './env' +import { audiusLibs, waitForLibsInit } from './libs' +import { localStorage } from './local-storage' + +/** + * Wraps a normal libs method call with method that calls the + * provided eager variant if libs is not already loaded. + * In the case the eager version returns an error, we wait for + * libs to inititalize and then call the normal method. + */ +export const withEagerOption = async ( + { + normal, + eager, + endpoint, + requiresUser = false + }: { + normal: (libs: any) => any + eager: (...args: any) => any + endpoint?: string + requiresUser?: boolean + }, + ...args: any +) => { + const disprovEndpoint = + endpoint ?? (await getEagerDiscprov(localStorage, env)) + if (audiusLibs) { + return normal(audiusLibs)(...args) + } else { + try { + const req = eager(...args) + const res = await makeEagerRequest( + req, + disprovEndpoint as string, + requiresUser, + localStorage, + env + ) + return res + } catch (e) { + await waitForLibsInit() + return normal(audiusLibs)(...args) + } + } +} diff --git a/packages/mobile/src/services/libs.ts b/packages/mobile/src/services/libs.ts new file mode 100644 index 0000000000..8964d1f5d7 --- /dev/null +++ b/packages/mobile/src/services/libs.ts @@ -0,0 +1,36 @@ +import EventEmitter from 'events' + +import type { AudiusLibs } from '@audius/sdk/dist/native-libs' + +// TODO: declare this at the root and use actual audiusLibs type +declare global { + interface Window { + audiusLibs: any + } +} + +export const libsInitEventEmitter = new EventEmitter() + +export let audiusLibs: AudiusLibs + +export const LIBS_INITTED_EVENT = 'LIBS_INITTED_EVENT' + +export const setLibs = (libs: AudiusLibs) => (audiusLibs = libs) + +/** + * Wait for the `LIBS_INITTED_EVENT` or pass through if there + * already exists a mounted `window.audiusLibs` object. + */ +export const waitForLibsInit = async () => { + // If libs is already defined, it has already loaded & initted + // so do nothing + if (audiusLibs) return + // Add an event listener and resolve when that returns + return new Promise((resolve) => { + if (audiusLibs) { + resolve() + } else { + libsInitEventEmitter.addListener(LIBS_INITTED_EVENT, resolve) + } + }) +} diff --git a/packages/web/src/services/audius-backend/eagerLoadUtils.ts b/packages/web/src/services/audius-backend/eagerLoadUtils.ts index c07b6908aa..8ab186d9cb 100644 --- a/packages/web/src/services/audius-backend/eagerLoadUtils.ts +++ b/packages/web/src/services/audius-backend/eagerLoadUtils.ts @@ -3,7 +3,7 @@ * protocol before libs has initialized. */ -import { getEagerDiscprov } from '@audius/common' +import { getEagerDiscprov, makeEagerRequest } from '@audius/common' import { env } from 'services/env' import { localStorage } from 'services/local-storage' @@ -56,10 +56,12 @@ export const withEagerOption = async ( } else { try { const req = eager(...args) - const res = await makeRequest( + const res = await makeEagerRequest( req, disprovEndpoint as string, - requiresUser + requiresUser, + localStorage, + env ) return res } catch (e) { @@ -69,80 +71,3 @@ export const withEagerOption = async ( } } } - -/** - * Convertsa a provided URL params object to a query string - */ -const parmsToQS = ( - params: { [key: string]: string | string[] }, - formatWithoutArray = false -) => { - if (!params) return '' - return Object.keys(params) - .map((k) => { - if (Array.isArray(params[k])) { - return (params[k] as string[]) - .map((val: string) => - formatWithoutArray - ? `${encodeURIComponent(k)}=${encodeURIComponent(val)}` - : `${encodeURIComponent(k)}[]=${encodeURIComponent(val)}` - ) - .join('&') - } - return ( - encodeURIComponent(k) + '=' + encodeURIComponent(params[k] as string) - ) - }) - .join('&') -} - -/** - * Takes a request object provided from the audius libs API and makes the request - * using the fetch API. - */ -const makeRequest = async ( - req: any, - endpoint: string, - requiresUser = false -) => { - const eagerDiscprov = await getEagerDiscprov(localStorage, env) - const discprovEndpoint = endpoint ?? eagerDiscprov - const user = await localStorage.getAudiusAccountUser() - if (!user && requiresUser) throw new Error('User required to continue') - - const headers: { [key: string]: string } = {} - if (user && user.user_id) { - headers['X-User-ID'] = user.user_id - } - - let baseUrl = `${discprovEndpoint}/${req.endpoint}` - if (req.urlParams) { - baseUrl = `${baseUrl}${req.urlParams}` - } - - let res: any - if (req?.method?.toLowerCase() === 'post') { - headers['Content-Type'] = 'application/json' - const url = `${baseUrl}?${parmsToQS( - req.queryParams, - discprovEndpoint === eagerDiscprov - )}` - res = await fetch(url, { - method: 'POST', - headers, - body: JSON.stringify(req.data) - }) - } else { - const url = `${baseUrl}?${parmsToQS( - req.queryParams, - discprovEndpoint === eagerDiscprov - )}` - res = await fetch(url, { - headers - }) - } - - const json = await res.json() - if (json.data) return json.data - return json -}