domain: UserName regex and webmention Selectors are recompiled on every call #70

Closed
opened 2026-07-03 01:43:18 +00:00 by rosa · 1 comment
Owner

Found during a code review of the domain crate.

Location: crates/domain/src/models/user.rs:43 (UserName::parse); crates/domain/src/models/webmention.rs:88 and :368
Type: Cleanup — wasted work

Problem

UserName::parse compiles Regex::new(r"^[a-z0-9]+$") on every call (the crate only regex use), and scrape_targets/classify_mention rebuild Selector::parse("a") / Selector::parse("a[href]") on every call. All are constants.

Cost

Per-call regex/selector construction on paths hit for every username parse and every webmention scrape (twice per post update).

Suggested fix

Hoist the selectors into static ... LazyLock<Selector>. For the username check, either a LazyLock<Regex> or, simpler, drop the regex for trimmed.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) (the following length check already rejects empty), and drop the regex dependency.

Found during a code review of the `domain` crate. **Location:** `crates/domain/src/models/user.rs:43` (`UserName::parse`); `crates/domain/src/models/webmention.rs:88` and `:368` **Type:** Cleanup — wasted work ## Problem `UserName::parse` compiles `Regex::new(r"^[a-z0-9]+$")` on every call (the crate only regex use), and `scrape_targets`/`classify_mention` rebuild `Selector::parse("a")` / `Selector::parse("a[href]")` on every call. All are constants. ## Cost Per-call regex/selector construction on paths hit for every username parse and every webmention scrape (twice per post update). ## Suggested fix Hoist the selectors into `static ... LazyLock<Selector>`. For the username check, either a `LazyLock<Regex>` or, simpler, drop the regex for `trimmed.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())` (the following length check already rejects empty), and drop the `regex` dependency.
Author
Owner

Agent Brief

Category: enhancement
Summary: Stop recompiling constant regexes/selectors on every call; construct them once (or eliminate them)

Current behavior:
Three call paths rebuild compile-time-constant patterns on every invocation:

  • UserName::parse compiles the ^[a-z0-9]+$ regex each time it runs. It is the domain crate's only use of regex.
  • In the domain crate's webmention model, scrape_targets rebuilds the a selector and classify_mention rebuilds the a[href] selector on every call. Both run for every webmention scrape (twice per post update).
  • In the infra crate, the webmention sender's endpoint-discovery step rebuilds the link[rel=webmention] and a[rel=webmention] selectors for every fetched target.

Desired behavior:
Constant patterns are constructed at most once for the life of the process, or eliminated. The repo already has the idiom to copy: static ... : LazyLock<...> as used for USERNAME_REGEX in the web crate's form extractors and for the processed stylesheet.

For UserName::parse, prefer the simpler option from the issue: drop the regex entirely in favor of a character check like trimmed.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()), and remove the regex dependency from the domain crate's manifest (the workspace keeps regex for the web crate). Note one subtlety: chars().all(...) is vacuously true for an empty string, so empty/whitespace-only input would then be rejected by the existing minimum-length check instead of the charset check — the issue explicitly accepts this; the input must still be rejected either way.

Key interfaces:

  • UserName::parse — accepted/rejected inputs are unchanged: trim, lowercase, only [a-z0-9], length 3–64. No signature change.
  • scrape_targets and classify_mention in the domain webmention model — behavior and signatures unchanged; only the selector construction moves.
  • The infra webmention sender's endpoint discovery — behavior unchanged (Link header still preferred, then link[rel=webmention], then a[rel=webmention]); only the selector construction moves.

Acceptance criteria:

  • No per-call Regex::new or Selector::parse of a constant pattern remains in the domain crate or the infra webmention sender
  • UserName::parse accepts and rejects the same set of inputs as before (empty and whitespace-only input still rejected)
  • If the character-check option is taken, the domain crate no longer depends on regex
  • All existing tests pass; the ci and clippy mise tasks pass

Out of scope:

  • Changing username validation rules (charset, 3–64 length) or any webmention behavior
  • Hoisting Html::parse_document / parse_fragment calls — those parse per-input HTML and are not constants
  • The web crate's regexes — they already use LazyLock
## Agent Brief **Category:** enhancement **Summary:** Stop recompiling constant regexes/selectors on every call; construct them once (or eliminate them) **Current behavior:** Three call paths rebuild compile-time-constant patterns on every invocation: - `UserName::parse` compiles the `^[a-z0-9]+$` regex each time it runs. It is the domain crate's only use of `regex`. - In the domain crate's webmention model, `scrape_targets` rebuilds the `a` selector and `classify_mention` rebuilds the `a[href]` selector on every call. Both run for every webmention scrape (twice per post update). - In the infra crate, the webmention sender's endpoint-discovery step rebuilds the `link[rel=webmention]` and `a[rel=webmention]` selectors for every fetched target. **Desired behavior:** Constant patterns are constructed at most once for the life of the process, or eliminated. The repo already has the idiom to copy: `static ... : LazyLock<...>` as used for `USERNAME_REGEX` in the web crate's form extractors and for the processed stylesheet. For `UserName::parse`, prefer the simpler option from the issue: drop the regex entirely in favor of a character check like `trimmed.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())`, and remove the `regex` dependency from the domain crate's manifest (the workspace keeps `regex` for the web crate). Note one subtlety: `chars().all(...)` is vacuously true for an empty string, so empty/whitespace-only input would then be rejected by the existing minimum-length check instead of the charset check — the issue explicitly accepts this; the input must still be rejected either way. **Key interfaces:** - `UserName::parse` — accepted/rejected inputs are unchanged: trim, lowercase, only `[a-z0-9]`, length 3–64. No signature change. - `scrape_targets` and `classify_mention` in the domain webmention model — behavior and signatures unchanged; only the selector construction moves. - The infra webmention sender's endpoint discovery — behavior unchanged (Link header still preferred, then `link[rel=webmention]`, then `a[rel=webmention]`); only the selector construction moves. **Acceptance criteria:** - [ ] No per-call `Regex::new` or `Selector::parse` of a constant pattern remains in the domain crate or the infra webmention sender - [ ] `UserName::parse` accepts and rejects the same set of inputs as before (empty and whitespace-only input still rejected) - [ ] If the character-check option is taken, the domain crate no longer depends on `regex` - [ ] All existing tests pass; the `ci` and `clippy` mise tasks pass **Out of scope:** - Changing username validation rules (charset, 3–64 length) or any webmention behavior - Hoisting `Html::parse_document` / `parse_fragment` calls — those parse per-input HTML and are not constants - The web crate's regexes — they already use `LazyLock`
rosa 2026-07-04 06:18:09 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rosa/vernier#70
No description provided.