refactor(分頁): 把逐頁走訪抽成 lib.pages
findIssueByTitle 原本自己寫了一份「翻到短頁為止、超過上限就報錯」的迴圈, 而新的留言走訪需要同一套規則。抽成非同步產生器之後,呼叫端仍能在找到目標時 提早離開,規則卻只寫一次。 錯誤碼與訊息由呼叫端指定:查重讀不完要講的是「可能重建議題」,數留言讀不完 要講的是「數不完未整併的則數」,處置不同就不該共用一句話。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+47
-16
@@ -443,25 +443,56 @@ export async function listLabels(login, repo) {
|
||||
* @param {string} title 要找的標題
|
||||
* @returns {Promise<object|null>} 找到的議題,或 null
|
||||
*/
|
||||
export async function findIssueByTitle(login, repo, title) {
|
||||
const wanted = title.trim();
|
||||
const pageSize = 50;
|
||||
// 翻頁上限。查不完就停手並報錯,不能無聲回 null——那會讓呼叫端把既有議題再建一次,
|
||||
// 正好是冪等查重要防的事。
|
||||
const maxPages = 200;
|
||||
/**
|
||||
* 逐頁走訪一個回傳陣列的 Gitea 端點。
|
||||
*
|
||||
* 所有「必須看完整份清單」的走訪都走這裡:讀不完就要大聲報錯,不能無聲回傳半份。
|
||||
* 半份清單比報錯更危險——查重會漏掉既有議題而重建一顆,數留言會少算而讓下游
|
||||
* 拿著過期描述做事。
|
||||
* @param {{base: string, token: string}} login
|
||||
* @param {string} path
|
||||
* @param {{query?: object, pageSize?: number, maxPages?: number, limitCode?: string, limitHint?: string}} options
|
||||
* @returns {AsyncGenerator<object[]>} 每次 yield 一頁
|
||||
*/
|
||||
export async function* pages(login, path, options = {}) {
|
||||
const {
|
||||
query = {},
|
||||
pageSize = 50,
|
||||
maxPages = 200,
|
||||
limitCode = 'PAGE_LIMIT',
|
||||
limitHint = `${path} 的資料量超出可走訪範圍`,
|
||||
} = options;
|
||||
|
||||
for (let page = 1; page <= maxPages; page += 1) {
|
||||
const response = await giteaRequest(login, 'GET', `/repos/${repo}/issues`, {
|
||||
query: { state: 'all', limit: pageSize, page },
|
||||
const response = await giteaRequest(login, 'GET', path, {
|
||||
query: { ...query, limit: pageSize, page },
|
||||
});
|
||||
const issues = expectOk(response, `GET /repos/${repo}/issues`) ?? [];
|
||||
const hit = issues.find((issue) => (issue.title ?? '').trim() === wanted);
|
||||
if (hit) return hit;
|
||||
if (issues.length < pageSize) return null;
|
||||
const items = expectOk(response, `GET ${path}`) ?? [];
|
||||
yield items;
|
||||
if (items.length < pageSize) return;
|
||||
}
|
||||
|
||||
throw new ScriptError(
|
||||
'DEDUPE_LIMIT',
|
||||
`翻完 ${maxPages * pageSize} 筆議題仍未查完 ${repo},無法確認「${wanted}」是否已存在;請縮小範圍或手動確認`,
|
||||
);
|
||||
throw new ScriptError(limitCode, `${limitHint}(已讀 ${maxPages * pageSize} 筆仍未讀完)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 依標題找出既有議題,讓寫入型腳本中斷重跑時不會產生重複議題。
|
||||
* 比對前後空白修掉:同一顆議題不該因為標題多了一個空格就被當成新的。
|
||||
* @param {{base: string, token: string}} login
|
||||
* @param {string} repo owner/name
|
||||
* @param {string} title 要找的標題
|
||||
* @returns {Promise<object|null>} 找到的議題,或 null
|
||||
*/
|
||||
export async function findIssueByTitle(login, repo, title) {
|
||||
const wanted = title.trim();
|
||||
|
||||
for await (const issues of pages(login, `/repos/${repo}/issues`, {
|
||||
query: { state: 'all' },
|
||||
limitCode: 'DEDUPE_LIMIT',
|
||||
limitHint: `翻不完 ${repo} 的議題,無法確認「${wanted}」是否已存在;請縮小範圍或手動確認`,
|
||||
})) {
|
||||
const hit = issues.find((issue) => (issue.title ?? '').trim() === wanted);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user