import isestr from 'wsemi/src/isestr.mjs'
import isobj from 'wsemi/src/isobj.mjs'
//抓取端之契約:本模組為其唯一事實來源
//
//對稱性補完:本套件原有兩個使用端擴充點各自有完整的檢核與錯誤邊界——
//adapter之parse由adapterContract把關、判識器由detectorContract把關——
//**抓取這一端卻沒有**。四個內建抓取器不拋錯故不出事,但:
// 一、opt._fetchers是文件化的測試接縫,接的就是呼叫端寫的函數
// 二、adapter之fetch掛點接上後,抓取器就是呼叫端程式碼
//
//實測三條路徑(改動前):
// 抓取器拋錯 → fetchWeb **reject**,違反其JSDoc明載之「本函數不會reject」
// 抓取器reject → 同上
// 回傳非物件 → status:'error' reason:'unknown',歸因不實(它不是「無上游歸因」,是壞掉)
//
//依全域§8第四類契約缺陷(有A→B無B→A:兩個擴充點有邊界,第三個沒有),不需已被觀察即應修
//
//── 抓取器之輸出契約 ──
// status 'success'或'error'
// 成功時 html 須為非空字串(否則後續判識與解析拿到的不是HTML)
// 失敗時 reason/message 保留其自述值
// 其餘欄位(method、snapshot、contentKind)由既有之adapt投影,本模組不重複處置
//
//── 錯誤邊界 ──
// 拋錯、reject、回傳非物件、成功卻無html → 一律轉為結構化失敗結果,絕不外洩為reject
/**
* 檢核抓取器之回傳值是否符合輸出契約
*
* 不合契約者轉為結構化失敗結果而非拋錯,使fetchWeb之「不會reject」契約於任何抓取器實作下皆成立
*
* @param {*} r 輸入抓取器之回傳值
* @param {String} reason 輸入不合契約時採用之失敗歸因字串
* @param {String} pre 輸入錯誤訊息前綴字串
* @returns {Object|null} 合契約時回傳null,不合時回傳{status:'error',reason,message}
* @example
*
* import { checkFetched } from './src/fetchContract.mjs'
*
* console.log(checkFetched({ status: 'success', html: '<p>a</p>' }, 'fetcher-error', 'curl '))
* // => null
*
* console.log(checkFetched('oops', 'fetcher-error', 'curl '))
* // => { status: 'error', reason: 'fetcher-error', message: 'curl returned invalid result' }
*
*/
function checkFetched(r, reason, pre) {
if (!isobj(r)) {
return { status: 'error', reason, message: pre + 'returned invalid result' }
}
//僅檢核自稱成功者: 自稱失敗時其形狀由既有之summarizeFail處置
if (r.status !== 'success') {
return null
}
if (!isestr(r.html)) {
return { status: 'error', reason, message: pre + 'returned success without html string' }
}
return null
}
/**
* 以錯誤邊界執行抓取函數,確保其拋錯或reject不外洩
*
* @param {Function} fn 輸入抓取async函數
* @param {String} url 輸入待抓取網址字串
* @param {Object} opt 輸入轉傳給抓取函數之設定物件
* @param {String} reason 輸入失敗時採用之歸因字串
* @param {String} pre 輸入錯誤訊息前綴字串
* @returns {Promise} 回傳Promise,resolve回傳抓取器之結果物件或結構化失敗結果,本函數不會reject
*/
async function runFetchSafely(fn, url, opt, reason, pre) {
let r
try {
r = await fn(url, opt)
}
catch (err) {
//await同時涵蓋同步拋錯與async reject
return { status: 'error', reason, message: pre + 'error: ' + (err?.message || String(err)) }
}
return checkFetched(r, reason, pre) || r
}
export {
checkFetched,
runFetchSafely
}