From 98c87a5d7002fe1c085718d271a44934b5284211 Mon Sep 17 00:00:00 2001 From: Chaim Marcus Date: Sun, 17 May 2026 10:15:51 +0000 Subject: [PATCH] perf: build case-issue map once in stale-case-reminder; add idempotency TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hoist issues.list() loop outside stale-cases loop — reduces RPCs from O(cases × companies × issues) to O(companies × issues) - Add TODO comment for requestId-based idempotency guard in onWebhook Co-Authored-By: Claude Sonnet 4.6 --- src/worker.ts | 54 +++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/worker.ts b/src/worker.ts index 6aefea2..5f2701a 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -722,36 +722,37 @@ const plugin = definePlugin({ total: number; }; - let reminded = 0; + // Build case→issue map once (O(companies × issues)) to avoid N×M RPCs per stale case const companies = await ctx.companies.list(); - for (const staleCase of data.cases) { - for (const company of companies) { - const issues = await ctx.issues.list({ companyId: company.id }); - let linkedIssueId: string | null = null; - for (const issue of issues) { - const linkedCase = await ctx.state.get({ - scopeKind: "issue", - scopeId: issue.id, - stateKey: "legal-case-number", - }); - if (linkedCase === staleCase.case_number) { - linkedIssueId = issue.id; - break; - } + const caseIssueMap = new Map(); + for (const company of companies) { + const issues = await ctx.issues.list({ companyId: company.id }); + for (const issue of issues) { + const linkedCase = await ctx.state.get({ + scopeKind: "issue", + scopeId: issue.id, + stateKey: "legal-case-number", + }); + if (linkedCase && typeof linkedCase === "string") { + caseIssueMap.set(linkedCase, { issueId: issue.id, companyId: company.id }); } - if (!linkedIssueId) continue; - - await ctx.issues.createComment( - linkedIssueId, - `⚠️ **תיק תקוע ${staleCase.case_number}** — ${staleCase.days_stale} ימים ללא עדכון (סטטוס: ${staleCase.status}). האם נדרשת פעולה?`, - company.id, - ); - reminded++; - ctx.logger.info(`stale-case-reminder: reminded case ${staleCase.case_number} (${staleCase.days_stale}d)`); - break; // Found the company, move to next case } } + let reminded = 0; + for (const staleCase of data.cases) { + const linked = caseIssueMap.get(staleCase.case_number); + if (!linked) continue; + + await ctx.issues.createComment( + linked.issueId, + `⚠️ **תיק תקוע ${staleCase.case_number}** — ${staleCase.days_stale} ימים ללא עדכון (סטטוס: ${staleCase.status}). האם נדרשת פעולה?`, + linked.companyId, + ); + reminded++; + ctx.logger.info(`stale-case-reminder: reminded case ${staleCase.case_number} (${staleCase.days_stale}d)`); + } + ctx.logger.info(`stale-case-reminder: done. ${reminded}/${data.total} cases reminded`); }); @@ -801,6 +802,9 @@ const plugin = definePlugin({ async onWebhook(input: PluginWebhookInput): Promise { if (!pluginCtx) return; // not yet initialized + // TODO: add idempotency guard using input.requestId to prevent duplicate + // comments on rapid retries (store requestId in plugin state with ~60s TTL) + const { endpointKey, parsedBody } = input; if (endpointKey !== "case-status") return;