sanity-plugin-mux-input
Version:
An input component that integrates Sanity Studio with Mux video encoding/hosting service.
169 lines (151 loc) • 4.42 kB
text/typescript
import {useEffect, useState} from 'react'
import {defer, of, timer} from 'rxjs'
import {concatMap, expand, tap} from 'rxjs/operators'
import type {MuxAsset, Secrets} from '../util/types'
const FIRST_PAGE = 1
const ASSETS_PER_PAGE = 100
type MuxAssetsState = {
pageNum: number
loading: boolean
data?: MuxAsset[]
error?: FetchError
}
type FetchError =
| {
_tag: 'FetchError'
}
| {_tag: 'MuxError'; error: unknown}
type PageResult = (
| {
data: MuxAsset[]
}
| {
error: FetchError
}
) & {
pageNum: number
}
/**
* @docs {@link https://docs.mux.com/api-reference#video/operation/list-assets}
*/
async function fetchMuxAssetsPage(
{secretKey, token}: Secrets,
pageNum: number
): Promise<PageResult> {
try {
const res = await fetch(
`https://api.mux.com/video/v1/assets?limit=${ASSETS_PER_PAGE}&page=${pageNum}`,
{
headers: {
Authorization: `Basic ${btoa(`${token}:${secretKey}`)}`,
},
}
)
const json = await res.json()
if (json.error) {
return {
pageNum,
error: {
_tag: 'MuxError',
error: json.error,
},
}
}
return {
pageNum,
data: json.data as MuxAsset[],
}
} catch (error) {
return {
pageNum,
error: {_tag: 'FetchError'},
}
}
}
function accumulateIntermediateState(
currentState: MuxAssetsState,
pageResult: PageResult
): MuxAssetsState {
const currentData = ('data' in currentState && currentState.data) || []
return {
...currentState,
data: [
...currentData,
...(('data' in pageResult && pageResult.data) || []).filter(
// De-duplicate assets for safety
(asset) => !currentData.some((a) => a.id === asset.id)
),
],
error:
'error' in pageResult
? pageResult.error
: // Reset error if current page is successful
undefined,
pageNum: pageResult.pageNum,
loading: true,
}
}
function hasMorePages(pageResult: PageResult) {
return (
typeof pageResult === 'object' &&
'data' in pageResult &&
Array.isArray(pageResult.data) &&
pageResult.data.length > 0
)
}
/**
* Fetches all assets from a Mux environment. Rules:
* - One page at a time
* - Mux has no information on pagination
* - We've finished fetching if a page returns `data.length === 0`
* - Rate limiting to one request per 2 seconds
* - Update state while still fetching to give feedback to users
*/
export default function useMuxAssets({secrets, enabled}: {enabled: boolean; secrets: Secrets}) {
const [state, setState] = useState<MuxAssetsState>({loading: true, pageNum: FIRST_PAGE})
useEffect(() => {
if (!enabled) return
const subscription = defer(() =>
fetchMuxAssetsPage(
secrets,
// When we've already successfully loaded before (fully or partially), we start from the following page to avoid re-fetching
'data' in state && state.data && state.data.length > 0 && !state.error
? state.pageNum + 1
: state.pageNum
)
)
.pipe(
// Here we replace "concatMap" with "expand" to recursively fetch next pages
expand((pageResult) => {
// if fetched page has data, we continue emitting, requesting the next page
// after 2s to avoid rate limiting
if (hasMorePages(pageResult)) {
return timer(2000).pipe(
// eslint-disable-next-line max-nested-callbacks
concatMap(() => defer(() => fetchMuxAssetsPage(secrets, pageResult.pageNum + 1)))
)
}
// Else, we stop emitting
return of()
}),
// On each iteration, persist intermediate states to give feedback to users
tap((pageResult) =>
setState((prevState) => accumulateIntermediateState(prevState, pageResult))
)
)
.subscribe({
// Once done, let the user know we've stopped loading
complete: () => {
setState((prev) => ({
...prev,
loading: false,
}))
},
})
// Unsubscribe on component unmount to prevent memory leaks or fetching unnecessarily
// eslint-disable-next-line consistent-return
return () => subscription.unsubscribe()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled])
return state
}