Files
website/src/app/(payload)/admin/_components/eversolo-paste-url-fetch-provider.tsx
T
2026-06-01 14:30:43 +08:00

285 lines
6.5 KiB
TypeScript

'use client'
import { useEffect } from 'react'
const allowedHosts = new Set(['www.eversolo.com', 'eversolo.com'])
const proxyPath = '/api/eversolo-paste-url'
const pasteFetchCollectDelay = 300
const pasteFetchIdleReleaseDelay = 250
const pasteFetchRetryDelays = [0, 300, 800]
const pasteFetchConcurrency = 4
const pasteFetchPatchVersion = 5
const bypassPathPrefixes = ['/admin', '/api', '/_next']
const assetPathPrefixes = ['/Attached/', '/Products/', '/Uploads/', '/controlapp/', '/files/', '/media/', '/videos/']
const assetExtensions = new Set([
'7z',
'aac',
'avi',
'bmp',
'css',
'csv',
'doc',
'docx',
'flac',
'gif',
'gz',
'htm',
'html',
'ico',
'jpeg',
'jpg',
'js',
'json',
'm4a',
'mov',
'mp3',
'mp4',
'pdf',
'png',
'rar',
'svg',
'tar',
'txt',
'wav',
'webm',
'webp',
'xls',
'xlsx',
'xml',
'zip',
])
type PatchedWindow = Window & {
__eversoloPasteURLFetchPatchVersion?: number
}
type QueuedPasteFetch = {
init?: RequestInit
proxiedURL: string
reject: (reason?: unknown) => void
resolve: (response: Response) => void
result?: PromiseSettledResult<Response>
source?: QueuedPasteFetch
}
let activePasteFetches = 0
let pasteFetchCollectTimer: number | undefined
let pasteFetchReleaseTimer: number | undefined
const pasteFetchPendingQueue: QueuedPasteFetch[] = []
let pasteFetchSession: QueuedPasteFetch[] = []
function getFetchURL(input: RequestInfo | URL) {
if (typeof input === 'string') return input
if (input instanceof URL) return input.href
return input.url
}
function getFetchMethod(input: RequestInfo | URL, init?: RequestInit) {
if (init?.method) return init.method
if (typeof input !== 'string' && !(input instanceof URL)) return input.method
return undefined
}
function isBypassedURL(url: URL) {
if (url.origin === window.location.origin) return true
if (url.searchParams.has('_rsc')) return true
return bypassPathPrefixes.some((prefix) => {
return url.pathname === prefix || url.pathname.startsWith(`${prefix}/`)
})
}
function isAssetURL(url: URL) {
if (assetPathPrefixes.some((prefix) => url.pathname.startsWith(prefix))) return true
const extension = url.pathname.split('/').pop()?.split('.').pop()?.toLowerCase()
return extension ? assetExtensions.has(extension) : false
}
function getProxiedPasteURL(input: RequestInfo | URL, init?: RequestInit) {
const method = getFetchMethod(input, init)
if (method && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
return null
}
try {
const url = new URL(getFetchURL(input))
if (url.protocol !== 'https:' || !allowedHosts.has(url.hostname)) {
return null
}
if (isBypassedURL(url) || !isAssetURL(url)) {
return null
}
return `${proxyPath}?src=${encodeURIComponent(url.href)}`
} catch {
return null
}
}
function wait(delay: number) {
return new Promise((resolve) => {
window.setTimeout(resolve, delay)
})
}
async function fetchWithRetry(
originalFetch: typeof fetch,
proxiedURL: string,
init?: RequestInit,
) {
let lastError: unknown
for (const [index, delay] of pasteFetchRetryDelays.entries()) {
if (delay > 0) {
await wait(delay)
}
try {
const response = await originalFetch(proxiedURL, {
...init,
method: 'GET',
})
if (response.ok || index === pasteFetchRetryDelays.length - 1) {
return response
}
} catch (error) {
lastError = error
}
}
throw lastError
}
function releasePasteFetchSession() {
if (activePasteFetches > 0 || pasteFetchPendingQueue.length > 0) return
const session = pasteFetchSession
pasteFetchSession = []
pasteFetchReleaseTimer = undefined
session.forEach((item) => {
const result = item.result ?? item.source?.result
if (!result) return
if (result.status === 'fulfilled') {
item.resolve(result.value.clone())
} else {
item.reject(result.reason)
}
})
}
function schedulePasteFetchRelease() {
if (activePasteFetches > 0 || pasteFetchPendingQueue.length > 0) return
if (pasteFetchReleaseTimer) {
window.clearTimeout(pasteFetchReleaseTimer)
}
pasteFetchReleaseTimer = window.setTimeout(() => {
releasePasteFetchSession()
}, pasteFetchIdleReleaseDelay)
}
function processPasteFetchQueue(originalFetch: typeof fetch) {
pasteFetchCollectTimer = undefined
while (activePasteFetches < pasteFetchConcurrency && pasteFetchPendingQueue.length > 0) {
const item = pasteFetchPendingQueue.shift()
if (!item) continue
activePasteFetches += 1
fetchWithRetry(originalFetch, item.proxiedURL, item.init)
.then((response) => {
item.result = {
status: 'fulfilled',
value: response,
}
})
.catch((reason: unknown) => {
item.result = {
reason,
status: 'rejected',
}
})
.finally(() => {
activePasteFetches -= 1
processPasteFetchQueue(originalFetch)
schedulePasteFetchRelease()
})
}
schedulePasteFetchRelease()
}
function enqueuePasteFetch(originalFetch: typeof fetch, proxiedURL: string, init?: RequestInit) {
return new Promise<Response>((resolve, reject) => {
if (pasteFetchReleaseTimer) {
window.clearTimeout(pasteFetchReleaseTimer)
pasteFetchReleaseTimer = undefined
}
const source = pasteFetchSession.find((pendingItem) => {
return !pendingItem.source && pendingItem.proxiedURL === proxiedURL
})
const item: QueuedPasteFetch = {
init,
proxiedURL,
reject,
resolve,
source,
}
pasteFetchSession.push(item)
if (source) {
schedulePasteFetchRelease()
return
}
pasteFetchPendingQueue.push(item)
if (pasteFetchCollectTimer) {
window.clearTimeout(pasteFetchCollectTimer)
}
pasteFetchCollectTimer = window.setTimeout(() => {
processPasteFetchQueue(originalFetch)
}, pasteFetchCollectDelay)
})
}
export function EversoloPasteURLFetchProvider({ children }: { children?: React.ReactNode }) {
useEffect(() => {
const patchedWindow = window as PatchedWindow
if (patchedWindow.__eversoloPasteURLFetchPatchVersion === pasteFetchPatchVersion) return
const originalFetch = window.fetch.bind(window)
patchedWindow.__eversoloPasteURLFetchPatchVersion = pasteFetchPatchVersion
window.fetch = (input, init) => {
const proxiedPasteURL = getProxiedPasteURL(input, init)
if (proxiedPasteURL) {
return enqueuePasteFetch(originalFetch, proxiedPasteURL, init)
}
return originalFetch(input, init)
}
}, [])
return children
}