Skip to content

Commit 9d7286b

Browse files
ralyodioclaude
andcommitted
pit: raise the paste ceiling to 1000 and stop on the clock, not a count
A real paste hit the old cap: 195 claimed, "389 past the 200 limit, not attempted" — and nothing said what to do about the 389. Two changes. The ceiling goes 200 -> 1000, but it is no longer the thing that usually stops a paste. Claiming now runs against a 20s budget and stops when the budget is spent. A count cannot know how slow the database is today; the failure it was guarding against was a request dying halfway with no report of what landed, and a clock guards that directly. A fast database gets through hundreds, a slow one stops early, and neither ends as a timed-out request whose result nobody sees. The budget is checked before each write and never before the first, so an already-slow database still claims one rather than reporting a paste that did nothing and looks broken. Whatever is left is now named and actionable: "N not attempted — paste them again to carry on", covering both the over-ceiling and out-of-time cases, which are the same problem from the user's side. Caught while testing: the single-ending shortcut in summarizeBulkClaim did not know about leftovers, so one claim plus four unattempted reported as ".x is yours." and silently lost the four. 6 more tests. 262 across the pwa suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 54402e8 commit 9d7286b

3 files changed

Lines changed: 96 additions & 11 deletions

File tree

apps/pwa/src/lib/moshpit-name.mjs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,27 @@ export function resolutionPreference({ registered, mode }) {
114114
}
115115

116116
/**
117-
* How many endings one paste may claim at a time.
117+
* The most endings one paste may claim.
118118
*
119-
* A cap rather than no cap because this runs one INSERT per ending against a
120-
* remote database, and a pasted spreadsheet column is exactly the shape of
121-
* input that turns into ten thousand of them by accident.
119+
* A ceiling rather than no ceiling because this runs one INSERT per ending
120+
* against a remote database, and a pasted spreadsheet column is exactly the
121+
* shape of input that turns into ten thousand of them by accident.
122+
*
123+
* It is not the thing that usually stops a paste, though — BULK_TIME_BUDGET_MS
124+
* is. A count cannot know how slow the database is today, and the failure it
125+
* guards against is a request that dies halfway with no report of what landed.
126+
*/
127+
export const MAX_BULK_TLDS = 1000;
128+
129+
/**
130+
* How long claiming may run before it stops and reports.
131+
*
132+
* Stopping on the clock rather than on a count adapts to the database: a fast
133+
* one gets through hundreds, a slow one stops early, and neither ends as a
134+
* timed-out request whose result nobody ever sees. Whatever is left is named
135+
* so it can be pasted again.
122136
*/
123-
export const MAX_BULK_TLDS = 200;
137+
export const BULK_TIME_BUDGET_MS = 20_000;
124138

125139
/**
126140
* The most a child name may cost per year.

apps/pwa/src/moshpit.mjs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
// checkable rather than trusted.
1212

1313
import { get, all, run } from "./db.mjs";
14-
import { MAX_BULK_TLDS, MAX_CHILD_PRICE_USD, normalizeLabel, normalizeTld, parseMoshpitName, parseTldList, tldRejection } from "./lib/moshpit-name.mjs";
14+
import { BULK_TIME_BUDGET_MS, MAX_BULK_TLDS, MAX_CHILD_PRICE_USD, normalizeLabel, normalizeTld, parseMoshpitName, parseTldList, tldRejection } from "./lib/moshpit-name.mjs";
1515

1616
export {
17-
RESERVED_TLDS, RESOLVE_MODES, MAX_BULK_TLDS, DEFAULT_TLD_PRICE_USD, MAX_CHILD_PRICE_USD, normalizeLabel, normalizeTld, parseMoshpitName,
17+
RESERVED_TLDS, RESOLVE_MODES, MAX_BULK_TLDS, BULK_TIME_BUDGET_MS, DEFAULT_TLD_PRICE_USD, MAX_CHILD_PRICE_USD, normalizeLabel, normalizeTld, parseMoshpitName,
1818
parseTldList, tldRejection, normalizeMode, resolutionPreference,
1919
} from "./lib/moshpit-name.mjs";
2020

@@ -578,16 +578,25 @@ export async function removePin({ tld: tldInput, label: labelInput, pin, userId
578578
*/
579579
export async function registerTlds({
580580
input, userId, ownerEmail = null, limit = MAX_BULK_TLDS, priceUsd = null, aliasOf = null,
581+
budgetMs = BULK_TIME_BUDGET_MS, now = Date.now,
581582
}) {
582583
const { entries, skipped } = parseTldList(input, limit);
584+
const deadline = now() + budgetMs;
585+
const remaining = [];
583586

584587
const claimed = [];
585588
const mine = [];
586589
const taken = [];
587590
const rejected = [];
588591
const settingsFailed = [];
589592

590-
for (const entry of entries) {
593+
for (const [index, entry] of entries.entries()) {
594+
// Checked before the write, not after: stopping with a claim half-made is
595+
// the one outcome worse than stopping early.
596+
if (index > 0 && now() >= deadline) {
597+
remaining.push(...entries.slice(index).map((e) => e.tld));
598+
break;
599+
}
591600
const tld = entry.tld;
592601
const result = await registerTld({ tld, userId, ownerEmail });
593602
if (result.ok) {
@@ -619,7 +628,7 @@ export async function registerTlds({
619628
rejected.push({ tld, error: result.error });
620629
}
621630

622-
return { claimed, mine, taken, rejected, settingsFailed, skipped, attempted: entries.length };
631+
return { claimed, mine, taken, rejected, settingsFailed, skipped, remaining, attempted: entries.length };
623632
}
624633

625634
/**
@@ -654,7 +663,8 @@ export function summarizeBulkClaim(result, limit = MAX_BULK_TLDS) {
654663
// batch report looks like, and the commonest path through this page is one
655664
// ending typed into one box.
656665
const onlyClaimed = result.claimed.length === 1 && !result.mine.length && !result.taken.length
657-
&& !result.rejected.length && !result.settingsFailed?.length && !result.skipped;
666+
&& !result.rejected.length && !result.settingsFailed?.length && !result.skipped
667+
&& !result.remaining?.length;
658668
if (onlyClaimed) return `.${result.claimed[0]} is yours.`;
659669

660670
const parts = [];
@@ -671,7 +681,10 @@ export function summarizeBulkClaim(result, limit = MAX_BULK_TLDS) {
671681
const first = result.settingsFailed[0];
672682
parts.push(`${result.settingsFailed.length} claimed but not configured (.${first.tld}${first.error})`);
673683
}
674-
if (result.skipped) parts.push(`${result.skipped} past the ${limit} limit, not attempted`);
684+
// The leftovers are the actionable part, so they say what to do rather than
685+
// just how many there were.
686+
const left = (result.remaining?.length || 0) + (result.skipped || 0);
687+
if (left) parts.push(`${left} not attempted — paste them again to carry on`);
675688

676689
return parts.length ? parts.join(". ") + "." : "nothing to claim — paste one ending per line.";
677690
}

apps/pwa/test/moshpit-bulk-claim.test.mjs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,61 @@ test("per-line settings beat the form", { skip: installed ? false : "pwa depende
340340
assert.equal(row.alias_of, hub);
341341
});
342342
});
343+
344+
test("a paste bigger than one request can finish", { skip: installed ? false : "pwa dependencies not installed" }, async (t) => {
345+
const { migrate } = await import("../src/migrate.mjs");
346+
await migrate();
347+
const { run } = await import("../src/db.mjs");
348+
await run(`INSERT OR IGNORE INTO users (id,email,created_at) VALUES (?,?,?)`, [ALICE, "alice@example.com", Date.now()]);
349+
const m = await import("../src/moshpit.mjs");
350+
const { BULK_TIME_BUDGET_MS, MAX_BULK_TLDS } = await import("../src/lib/moshpit-name.mjs");
351+
const uniq = () => `t${randomBytes(4).toString("hex")}`;
352+
353+
await t.test("the ceiling is 1000", () => {
354+
assert.equal(MAX_BULK_TLDS, 1000);
355+
assert.ok(BULK_TIME_BUDGET_MS > 0);
356+
});
357+
358+
await t.test("running out of time names what is left instead of dropping it", async () => {
359+
const names = Array.from({ length: 5 }, uniq);
360+
// A clock that jumps past the budget after the first claim.
361+
let calls = 0;
362+
const result = await m.registerTlds({
363+
input: names.join("\n"), userId: ALICE, budgetMs: 1000,
364+
now: () => (calls++ === 0 ? 0 : 99_999),
365+
});
366+
367+
assert.equal(result.claimed.length, 1, "the first one lands");
368+
assert.deepEqual(result.remaining, names.slice(1), "the rest are named, not lost");
369+
assert.match(m.summarizeBulkClaim(result), /4 not attempted paste them again/);
370+
});
371+
372+
await t.test("the budget is never checked before the first claim", async () => {
373+
// An already-expired clock must still do one, or a slow database means a
374+
// paste that claims nothing at all and looks broken.
375+
const one = uniq();
376+
const result = await m.registerTlds({
377+
input: one, userId: ALICE, budgetMs: 0, now: () => 99_999,
378+
});
379+
assert.deepEqual(result.claimed, [one]);
380+
assert.deepEqual(result.remaining, []);
381+
});
382+
383+
await t.test("a paste that fits reports nothing left over", async () => {
384+
const names = Array.from({ length: 3 }, uniq);
385+
const result = await m.registerTlds({ input: names.join("\n"), userId: ALICE });
386+
387+
assert.equal(result.claimed.length, 3);
388+
assert.deepEqual(result.remaining, []);
389+
assert.doesNotMatch(m.summarizeBulkClaim(result), /not attempted/);
390+
});
391+
392+
await t.test("over the ceiling still counts as left over, not dropped", async () => {
393+
const names = Array.from({ length: 4 }, uniq);
394+
const result = await m.registerTlds({ input: names.join("\n"), userId: ALICE, limit: 2 });
395+
396+
assert.equal(result.claimed.length, 2);
397+
assert.equal(result.skipped, 2);
398+
assert.match(m.summarizeBulkClaim(result), /2 not attempted paste them again/);
399+
});
400+
});

0 commit comments

Comments
 (0)