285 lines
6.5 KiB
TypeScript
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
|
|
}
|