diff options
| author | Adam Soltys <[email protected]> | 2026-06-18 07:41:21 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-18 08:27:11 -0700 |
| commit | fbf0c4f45249875b0b95eeb1beedd811b176a245 (patch) | |
| tree | 9f46283758b23e973ba7fd6f0bc1dd15f1cd8083 /scripts | |
| parent | 290acb73beff82762572d6eb69c6297bf7d88af7 (diff) | |
| download | coinos-server-fbf0c4f45249875b0b95eeb1beedd811b176a245.tar.gz coinos-server-fbf0c4f45249875b0b95eeb1beedd811b176a245.zip | |
scripts: add dedup-invoices cleanup for already-duplicated :invoices
The source fix (LPOS guard in generate()) stops NEW duplicate entries but
doesn't remove the ones already accumulated. This one-time script scrubs
them: scans every <id>:invoices list and removes duplicate ids, keeping
each id's original creation-order position. Dry-run by default; pass
--apply to write. Idempotent.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/dedup-invoices.ts | 86 |
1 files changed, 86 insertions, 0 deletions
diff --git a/scripts/dedup-invoices.ts b/scripts/dedup-invoices.ts new file mode 100644 index 00000000..9232566b --- /dev/null +++ b/scripts/dedup-invoices.ts @@ -0,0 +1,86 @@ +#!/usr/bin/env bun +// One-time cleanup: remove duplicate entries from every `<id>:invoices` list. +// +// Background: `generate()` used to `lPush` the invoice id onto the owner's +// `:invoices` list every time it ran — including when regenerating an existing +// invoice (adding a tip, webhook, etc.). That pushed the same id multiple times, +// so a user's `/invoices` response contained duplicate records. The UI's keyed +// `{#each}` then threw `each_key_duplicate` and rendered a blank page. The source +// is fixed in lib/invoices.ts (only push when the id is brand new); this script +// cleans the dupes that already accumulated. +// +// Dedup preserves the ORIGINAL position of each id. The list is newest-first +// (lPush adds to the head), and re-pushes added newer copies at the head, so the +// deepest (tail-most) occurrence is the original creation push — we keep that one. +// +// Read-only by default (dry run). Pass --apply to actually rewrite lists. +// Usage: bun scripts/dedup-invoices.ts [--apply] + +import { createClient } from "redis"; + +const apply = process.argv.includes("--apply"); + +const db = createClient({ + url: "redis://127.0.0.1:6379", + socket: { reconnectStrategy: () => false }, +}); +await db.connect(); + +// Keep the LAST occurrence of each id (original creation order), preserving +// the relative order of the kept entries. +function dedupeKeepLast(list: string[]): string[] { + const seen = new Set<string>(); + const out: string[] = []; + for (let i = list.length - 1; i >= 0; i--) { + const id = list[i]; + if (seen.has(id)) continue; + seen.add(id); + out.push(id); + } + out.reverse(); + return out; +} + +let lists = 0; +let listsWithDupes = 0; +let entriesBefore = 0; +let entriesRemoved = 0; +const worst: { key: string; before: number; after: number }[] = []; + +for await (const k of db.scanIterator({ MATCH: "*:invoices", COUNT: 2000 })) { + const keys = Array.isArray(k) ? k : [k]; + for (const key of keys) { + lists++; + const list = await db.lRange(key, 0, -1); + entriesBefore += list.length; + const deduped = dedupeKeepLast(list); + const removed = list.length - deduped.length; + if (removed === 0) continue; + + listsWithDupes++; + entriesRemoved += removed; + worst.push({ key, before: list.length, after: deduped.length }); + + if (apply) { + // Rewrite atomically: DEL then RPUSH the deduped list (RPUSH keeps head->tail order). + const tx = db.multi(); + tx.del(key); + tx.rPush(key, deduped); + await tx.exec(); + } + } +} + +worst.sort((a, b) => b.before - b.after - (a.before - a.after)); + +console.log(`\n${apply ? "APPLIED" : "DRY RUN (use --apply to write)"}`); +console.log(`lists scanned: ${lists}`); +console.log(`lists with dupes: ${listsWithDupes}`); +console.log(`entries before: ${entriesBefore}`); +console.log(`duplicate entries: ${entriesRemoved}`); +console.log(`\ntop 10 most-duplicated lists:`); +for (const w of worst.slice(0, 10)) { + console.log(` ${w.key} ${w.before} -> ${w.after} (-${w.before - w.after})`); +} + +await db.quit(); |
