perf: build case-issue map once in stale-case-reminder; add idempotency TODO

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 10:15:51 +00:00
parent 09acb021eb
commit 98c87a5d70

View File

@@ -722,34 +722,35 @@ const plugin = definePlugin({
total: number; 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(); const companies = await ctx.companies.list();
for (const staleCase of data.cases) { const caseIssueMap = new Map<string, { issueId: string; companyId: string }>();
for (const company of companies) { for (const company of companies) {
const issues = await ctx.issues.list({ companyId: company.id }); const issues = await ctx.issues.list({ companyId: company.id });
let linkedIssueId: string | null = null;
for (const issue of issues) { for (const issue of issues) {
const linkedCase = await ctx.state.get({ const linkedCase = await ctx.state.get({
scopeKind: "issue", scopeKind: "issue",
scopeId: issue.id, scopeId: issue.id,
stateKey: "legal-case-number", stateKey: "legal-case-number",
}); });
if (linkedCase === staleCase.case_number) { if (linkedCase && typeof linkedCase === "string") {
linkedIssueId = issue.id; caseIssueMap.set(linkedCase, { issueId: issue.id, companyId: company.id });
break;
} }
} }
if (!linkedIssueId) continue; }
let reminded = 0;
for (const staleCase of data.cases) {
const linked = caseIssueMap.get(staleCase.case_number);
if (!linked) continue;
await ctx.issues.createComment( await ctx.issues.createComment(
linkedIssueId, linked.issueId,
`⚠️ **תיק תקוע ${staleCase.case_number}** — ${staleCase.days_stale} ימים ללא עדכון (סטטוס: ${staleCase.status}). האם נדרשת פעולה?`, `⚠️ **תיק תקוע ${staleCase.case_number}** — ${staleCase.days_stale} ימים ללא עדכון (סטטוס: ${staleCase.status}). האם נדרשת פעולה?`,
company.id, linked.companyId,
); );
reminded++; reminded++;
ctx.logger.info(`stale-case-reminder: reminded case ${staleCase.case_number} (${staleCase.days_stale}d)`); ctx.logger.info(`stale-case-reminder: reminded case ${staleCase.case_number} (${staleCase.days_stale}d)`);
break; // Found the company, move to next case
}
} }
ctx.logger.info(`stale-case-reminder: done. ${reminded}/${data.total} cases reminded`); ctx.logger.info(`stale-case-reminder: done. ${reminded}/${data.total} cases reminded`);
@@ -801,6 +802,9 @@ const plugin = definePlugin({
async onWebhook(input: PluginWebhookInput): Promise<void> { async onWebhook(input: PluginWebhookInput): Promise<void> {
if (!pluginCtx) return; // not yet initialized 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; const { endpointKey, parsedBody } = input;
if (endpointKey !== "case-status") return; if (endpointKey !== "case-status") return;