CRLF mi bomboclaart

This commit is contained in:
Ismaël Moret
2026-04-21 17:10:33 +00:00
parent c77b184bea
commit 2c41f34c72

View File

@@ -1,243 +1,243 @@
/** /**
* Multifetch, adapted from "@ntlab/sfetch" by BellezaEmporium. * Multifetch, adapted from "@ntlab/sfetch" by BellezaEmporium.
* Multiple concurrent fetch with a callback to process the result of each request. * Multiple concurrent fetch with a callback to process the result of each request.
* The maximum number of concurrent workers can be configured with `setMaxWorker()`. * The maximum number of concurrent workers can be configured with `setMaxWorker()`.
* By default, the callback will only be called if the request is successful and returns a result. * By default, the callback will only be called if the request is successful and returns a result.
* This behavior can be changed with `setCheckResult()`. * This behavior can be changed with `setCheckResult()`.
* A custom debug function can be set with `setDebugger()`. * A custom debug function can be set with `setDebugger()`.
* Native mock support via `setMocks()` for testing without axios. * Native mock support via `setMocks()` for testing without axios.
*/ */
import axios, { AxiosRequestConfig } from 'axios' import axios, { AxiosRequestConfig } from 'axios'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
interface QueueItem { interface QueueItem {
url: string; url: string;
method?: string; method?: string;
params?: Record<string, unknown>; params?: Record<string, unknown>;
} }
type QueueEntry = string | QueueItem; type QueueEntry = string | QueueItem;
type Callback = (queue: QueueEntry, data: unknown, headers?: unknown) => void; type Callback = (queue: QueueEntry, data: unknown, headers?: unknown) => void;
type DebugFn = (format: string, url: string, config: string) => void; type DebugFn = (format: string, url: string, config: string) => void;
type MockHandler = (url: string, config?: AxiosRequestConfig) => unknown; type MockHandler = (url: string, config?: AxiosRequestConfig) => unknown;
interface MockRoute { interface MockRoute {
handler: MockHandler | string; handler: MockHandler | string;
dataDir?: string; dataDir?: string;
} }
let nworker = 25 let nworker = 25
let checkResult = true let checkResult = true
let debug: DebugFn | undefined let debug: DebugFn | undefined
const mocks = new Map<string, MockRoute>() const mocks = new Map<string, MockRoute>()
/** /**
* Process a mock route and return response * Process a mock route and return response
*/ */
const processMockRoute = (url: string, config: AxiosRequestConfig | undefined, route: MockRoute): unknown => { const processMockRoute = (url: string, config: AxiosRequestConfig | undefined, route: MockRoute): unknown => {
const { handler, dataDir } = route const { handler, dataDir } = route
if (typeof handler === 'function') { if (typeof handler === 'function') {
return handler(url, config) return handler(url, config)
} }
if (typeof handler === 'string') { if (typeof handler === 'string') {
// If it looks like a file path, read from disk // If it looks like a file path, read from disk
if (handler.includes('.') || handler.includes('/')) { if (handler.includes('.') || handler.includes('/')) {
const filePath = dataDir ? path.join(dataDir, handler) : handler const filePath = dataDir ? path.join(dataDir, handler) : handler
const content = fs.readFileSync(filePath, 'utf8') const content = fs.readFileSync(filePath, 'utf8')
if (handler.endsWith('.json')) { if (handler.endsWith('.json')) {
try { try {
return JSON.parse(content) return JSON.parse(content)
} catch { } catch {
return content return content
} }
} }
return content return content
} }
// Otherwise return as-is // Otherwise return as-is
return handler return handler
} }
return handler return handler
} }
/** /**
* Check if a URL matches any mock pattern * Check if a URL matches any mock pattern
*/ */
const findMock = (url: string): MockRoute | undefined => { const findMock = (url: string): MockRoute | undefined => {
// Try exact match first // Try exact match first
if (mocks.has(url)) return mocks.get(url) if (mocks.has(url)) return mocks.get(url)
// Try pattern matching (prefix match) // Try pattern matching (prefix match)
for (const [pattern, route] of mocks) { for (const [pattern, route] of mocks) {
if (url.startsWith(pattern)) return route if (url.startsWith(pattern)) return route
} }
return undefined return undefined
} }
async function doFetch(queues: QueueEntry[], cb: Callback) { async function doFetch(queues: QueueEntry[], cb: Callback) {
if (!queues.length) return if (!queues.length) return
let resolveFinish: (() => void) | undefined let resolveFinish: (() => void) | undefined
const workers = new Set<() => void>() const workers = new Set<() => void>()
let activeWorkers = 0 let activeWorkers = 0
const processQueue = () => { const processQueue = () => {
if (queues.length > 0 && activeWorkers < nworker) { if (queues.length > 0 && activeWorkers < nworker) {
const queue = queues.shift() const queue = queues.shift()
if (queue === undefined) return if (queue === undefined) return
activeWorkers++ activeWorkers++
const processRequest = async () => { const processRequest = async () => {
try { try {
const isQueueObject = typeof queue === 'object' && queue !== null const isQueueObject = typeof queue === 'object' && queue !== null
const url = isQueueObject ? queue.url : (queue as string) const url = isQueueObject ? queue.url : (queue as string)
const method = (isQueueObject && queue.method) ? queue.method : 'get' const method = (isQueueObject && queue.method) ? queue.method : 'get'
const params = (isQueueObject && queue.params) ? queue.params : {} const params = (isQueueObject && queue.params) ? queue.params : {}
const requestConfig: AxiosRequestConfig = method === 'request' const requestConfig: AxiosRequestConfig = method === 'request'
? { ...params, url } ? { ...params, url }
: { ...params, url, method: method as AxiosRequestConfig['method'] } : { ...params, url, method: method as AxiosRequestConfig['method'] }
if (debug) { if (debug) {
debug('fetch %s with %s', url, JSON.stringify(requestConfig)) debug('fetch %s with %s', url, JSON.stringify(requestConfig))
} }
// Check if there's a mock for this URL // Check if there's a mock for this URL
const mock = findMock(url) const mock = findMock(url)
let response: unknown let response: unknown
if (mock) { if (mock) {
const mockResponse = processMockRoute(url, requestConfig, mock) const mockResponse = processMockRoute(url, requestConfig, mock)
const isObj = typeof mockResponse === 'object' && mockResponse !== null const isObj = typeof mockResponse === 'object' && mockResponse !== null
if (debug) { if (debug) {
const hasData = isObj && 'data' in mockResponse const hasData = isObj && 'data' in mockResponse
const keys = isObj ? Object.keys(mockResponse).join(',') : '' const keys = isObj ? Object.keys(mockResponse).join(',') : ''
debug(`mock response type: ${typeof mockResponse}, has data: ${hasData}, keys: ${keys}`, url, JSON.stringify(requestConfig)) debug(`mock response type: ${typeof mockResponse}, has data: ${hasData}, keys: ${keys}`, url, JSON.stringify(requestConfig))
} }
// Check if response looks like it's already formatted (has 'data' and optionally 'status'/'headers') // Check if response looks like it's already formatted (has 'data' and optionally 'status'/'headers')
const isFormatted = isObj && 'data' in mockResponse && 'status' in mockResponse const isFormatted = isObj && 'data' in mockResponse && 'status' in mockResponse
response = isFormatted ? mockResponse : { data: mockResponse, status: 200, headers: {} } response = isFormatted ? mockResponse : { data: mockResponse, status: 200, headers: {} }
} else if (mocks.size > 0) { } else if (mocks.size > 0) {
// If mocks are set up but this URL doesn't match, return 404 // If mocks are set up but this URL doesn't match, return 404
response = { data: '', status: 404, headers: {} } response = { data: '', status: 404, headers: {} }
} else { } else {
// No mocks set up, use real axios // No mocks set up, use real axios
const axMethod = (requestConfig.method || 'get').toLowerCase() const axMethod = (requestConfig.method || 'get').toLowerCase()
if (axMethod === 'get' && typeof axios.get === 'function') { if (axMethod === 'get' && typeof axios.get === 'function') {
response = await axios.get(url, requestConfig) response = await axios.get(url, requestConfig)
} else if (axMethod === 'post' && typeof axios.post === 'function') { } else if (axMethod === 'post' && typeof axios.post === 'function') {
response = await axios.post(url, requestConfig.data, requestConfig) response = await axios.post(url, requestConfig.data, requestConfig)
} else { } else {
response = await axios.request(requestConfig) response = await axios.request(requestConfig)
} }
} }
const res = response as { data?: unknown; headers?: unknown } | undefined const res = response as { data?: unknown; headers?: unknown } | undefined
if ((checkResult && res?.data) || !checkResult) { if ((checkResult && res?.data) || !checkResult) {
cb(queue, res?.data, res?.headers) cb(queue, res?.data, res?.headers)
} }
} catch (err: unknown) { } catch (err: unknown) {
const url = typeof queue === 'object' ? queue.url : queue const url = typeof queue === 'object' ? queue.url : queue
const errorMessage = err instanceof Error ? err.message : String(err) const errorMessage = err instanceof Error ? err.message : String(err)
console.error(`Unable to fetch ${url}: ${errorMessage}!`) console.error(`Unable to fetch ${url}: ${errorMessage}!`)
if (!checkResult) { if (!checkResult) {
cb(queue, undefined) cb(queue, undefined)
} }
} finally { } finally {
activeWorkers-- activeWorkers--
workers.delete(processRequest) workers.delete(processRequest)
processQueue() processQueue()
if (activeWorkers === 0 && queues.length === 0 && resolveFinish) { if (activeWorkers === 0 && queues.length === 0 && resolveFinish) {
resolveFinish() resolveFinish()
} }
} }
} }
workers.add(processRequest) workers.add(processRequest)
processRequest() processRequest()
} }
} }
// Start initial workers // Start initial workers
const initialWorkers = Math.min(nworker, queues.length) const initialWorkers = Math.min(nworker, queues.length)
for (let i = 0; i < initialWorkers; i++) { for (let i = 0; i < initialWorkers; i++) {
processQueue() processQueue()
} }
// Wait for all to complete // Wait for all to complete
if (workers.size > 0 || activeWorkers > 0) { if (workers.size > 0 || activeWorkers > 0) {
await new Promise<void>(resolve => { await new Promise<void>(resolve => {
resolveFinish = resolve resolveFinish = resolve
}) })
} }
} }
Object.assign(doFetch, { Object.assign(doFetch, {
getMaxWorker() { getMaxWorker() {
return nworker return nworker
}, },
setMaxWorker(n: number) { setMaxWorker(n: number) {
nworker = n nworker = n
return doFetch return doFetch
}, },
getCheckResult() { getCheckResult() {
return checkResult return checkResult
}, },
setCheckResult(enabled: boolean) { setCheckResult(enabled: boolean) {
checkResult = enabled checkResult = enabled
return doFetch return doFetch
}, },
setDebugger(dbg: (arg0: string, arg1: unknown, arg2: string) => void) { setDebugger(dbg: (arg0: string, arg1: unknown, arg2: string) => void) {
debug = dbg debug = dbg
return doFetch return doFetch
}, },
/** /**
* Set mocks for URLs (for testing) * Set mocks for URLs (for testing)
* @param mockConfig - Object with URL patterns as keys and handlers as values * @param mockConfig - Object with URL patterns as keys and handlers as values
* @param dataDir - Optional directory for resolving file paths in handlers * @param dataDir - Optional directory for resolving file paths in handlers
* *
* Usage: * Usage:
* multifetch.setMocks({ * multifetch.setMocks({
* 'https://example.com/api': (url) => ({ data: 'response' }), * 'https://example.com/api': (url) => ({ data: 'response' }),
* 'https://example.com/file': 'response.json' * 'https://example.com/file': 'response.json'
* }, __dirname) * }, __dirname)
*/ */
setMocks(mockConfig: Record<string, MockHandler | string>, dataDir?: string) { setMocks(mockConfig: Record<string, MockHandler | string>, dataDir?: string) {
mocks.clear() mocks.clear()
for (const [url, handler] of Object.entries(mockConfig)) { for (const [url, handler] of Object.entries(mockConfig)) {
mocks.set(url, { handler, dataDir }) mocks.set(url, { handler, dataDir })
} }
return doFetch return doFetch
}, },
/** /**
* Add a single mock route * Add a single mock route
*/ */
addMock(url: string, handler: MockHandler | string, dataDir?: string) { addMock(url: string, handler: MockHandler | string, dataDir?: string) {
mocks.set(url, { handler, dataDir }) mocks.set(url, { handler, dataDir })
return doFetch return doFetch
}, },
/** /**
* Clear all mocks * Clear all mocks
*/ */
clearMocks() { clearMocks() {
mocks.clear() mocks.clear()
return doFetch return doFetch
}, },
/** /**
* Get registered mock URLs for debugging * Get registered mock URLs for debugging
*/ */
getMocks() { getMocks() {
return Array.from(mocks.keys()) return Array.from(mocks.keys())
} }
}) })
export default doFetch export default doFetch
module.exports = doFetch module.exports = doFetch