Skip to content

SEP-1779: Lift the duplicated search debounce into @sep/framework - #1354

Open
nachodd wants to merge 3 commits into
mainfrom
SEP-1779
Open

SEP-1779: Lift the duplicated search debounce into @sep/framework#1354
nachodd wants to merge 3 commits into
mainfrom
SEP-1779

Conversation

@nachodd

@nachodd nachodd commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

What

The Snippet Manager list and the ATW Collect pane's snippet picker each carried the same debounce: a useEffect opening a setTimeout, clearing it on cleanup, against its own locally-declared 300ms constant. Any change to the window had to be found and applied twice.

useDebouncedValue(value, delayMs) and the shared SEARCH_DEBOUNCE_MS now live in @sep/framework, and both consumers call it.

  • packages/framework/src/hooks/useDebouncedValue.ts (new) — the primitive plus the shared constant, exported from both barrels.
  • packages/apps/snippets/src/SnippetsListPage.tsx — inline block and local SEARCH_DEBOUNCE_MS dropped.
  • packages/apps/atw/src/CollectPane.tsx — inline block and local SNIPPET_SEARCH_DEBOUNCE_MS dropped.

Two deliberate, non-user-visible details:

  • The hook seeds its state from the incoming value rather than from a blank, so a list mounted with a term already in state queries for that term instead of fetching the unfiltered page first. Both consumers mount with an empty box today, so nothing moves in practice.
  • Callers trim at the call site. The old effects trimmed too, just inside the timer, so a trailing-space keystroke no longer restarts a window it would have published the same value from.

Otherwise identical: same window, same one-publish-per-pause coalescing, same clear on unmount.

Scope note: the search-hook half

The ticket also asked for a shared snippets-search query hook, on the premise that useAtwSnippetSearch and useSnippets were two copies of one query. That premise no longer holds on main. SEP-1821 (#1349) moved ATW's search onto ATW's own router: it now targets /apps/atw/snippets/ rather than SNIPPETS_APPS_API_BASE, sends no approval param (pinned server-side), and has no row projection (toAtwSnippetSummary was deleted as an identity map). useSnippets has meanwhile gained a sort param.

The two hooks therefore share no endpoint, no param surface, and no mapper. The only remaining overlap is apiClient.get -> normalizeAppListResponse -> keepPreviousData — which useTasksList shares equally. That is a generic app-list wrapper, not a snippets-search one, and worth its own ticket rather than being smuggled in under this name. The ticket's query-key invariants are moot as a result, but hold trivially: neither hook was touched, so the Snippet Manager's key shape is byte-identical and no ATW result lands under ['snippets', 'list'].

Acceptance criteria

  • @sep/framework exports useDebouncedValue plus the shared 300ms constant, replacing both local copies.
  • The Snippet Manager list drops its inline debounce block.
  • The ATW snippet picker drops its inline debounce block.
  • @sep/framework carries unit tests for the new export, covering timer behaviour and cleanup on unmount.
  • The existing @sep/snippets and @sep/atw suites pass with no behavioural edits — the only change is SnippetsListPage.test.tsx moving from a full vi.mock('@sep/framework') to an importOriginal partial mock, so the page exercises the real primitive while the download hook stays stubbed. The ATW suite needed no change at all.
  • Optimistic approval still updates every cached list page under its own filter and still rolls back — hooks.test.tsx passes unedited (useSnippets was not touched).
  • Shared snippets-search hook — see scope note above.

ScriptPreviewField.tsx's own DEBOUNCE_MS is left alone as the ticket directs: it debounces an effect with AbortController cancellation, not a value.

Test plan

  • pnpm --filter @sep/framework test — 762 pass, including the 6 new useDebouncedValue specs.
  • pnpm --filter @sep/snippets test — 72 pass.
  • pnpm --filter @sep/atw test — 83 pass.
  • pnpm --filter @sep/framework --filter @sep/snippets --filter @sep/atw type-check — clean.
  • oxlint — 0 errors on the full frontend.

Manually: type in the Snippet Manager search box and in the Collect pane's snippet picker; each fires one request per pause, not one per keystroke.

The Snippet Manager list and the ATW Collect pane's snippet picker each
carried the same debounce: a `useEffect` opening a `setTimeout`, clearing
it on cleanup, against its own locally-declared 300ms constant. Two
copies means a change to the window has to be found and applied twice,
and the second copy is the one that gets missed.

`useDebouncedValue(value, delayMs)` and the shared `SEARCH_DEBOUNCE_MS`
now live in `@sep/framework`, and both consumers call it. The primitive
seeds its state from the incoming value rather than from a blank, so a
list mounted with a term already in state queries for that term instead
of fetching the unfiltered page first; both consumers mount with an empty
box today, so nothing user-visible moves. Callers trim at the call site,
which is where the old effects trimmed too, only inside the timer.

Behaviour is otherwise unchanged: same window, same one-publish-per-pause
coalescing, same clear on unmount.

`SnippetsListPage.test.tsx` moves from a full `vi.mock('@sep/framework')`
to an `importOriginal` partial mock so the page exercises the real
primitive; only the download hook stays stubbed. The ATW suite needed no
change.

Scope note: the ticket also asked for a shared snippets-search query
hook. SEP-1821 (#1349) has since moved ATW's search onto ATW's own
router, dropping the `approval` param and the row projection, and
`useSnippets` has gained a `sort` param, so the two hooks no longer share
an endpoint, a param surface, or a mapper. The only remaining overlap is
`apiClient.get` -> `normalizeAppListResponse` -> `keepPreviousData`,
which `useTasksList` shares equally: a generic app-list wrapper, not a
snippets-search one. Left for a follow-up rather than built here.
@nachodd
nachodd requested a review from peter-o-addo as a code owner August 17, 2026 12:02
Copilot AI lite review requested due to automatic review settings August 17, 2026 12:02
@nachodd
nachodd requested a review from yyyyyyyan as a code owner August 17, 2026 12:02
@github-actions github-actions Bot added frontend app:atw PR touches the atw app slice app:snippets PR touches the snippets app slice labels Aug 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR centralizes the previously duplicated “search debounce” logic into @sep/framework by introducing a shared useDebouncedValue(value, delayMs) hook and a shared SEARCH_DEBOUNCE_MS constant, then updating both the Snippet Manager list and ATW Collect pane snippet picker to use the shared primitive.

Changes:

  • Added useDebouncedValue and SEARCH_DEBOUNCE_MS to @sep/framework and exported them via the hooks barrel and package entrypoint.
  • Replaced local useEffect + setTimeout debounce blocks in @sep/snippets and @sep/atw with useDebouncedValue(search.trim()).
  • Added unit tests in @sep/framework covering initial value behavior, timer coalescing, delay changes, and unmount cleanup; adjusted SnippetsListPage tests to partial-mock @sep/framework so the real debounce primitive is exercised.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
frontend/packages/framework/src/index.ts Re-exports the new hook and constant from the framework package entrypoint.
frontend/packages/framework/src/hooks/useDebouncedValue.ts Introduces the shared debounce hook and shared SEARCH_DEBOUNCE_MS constant.
frontend/packages/framework/src/hooks/useDebouncedValue.test.tsx Adds unit tests validating debounce timing and cleanup semantics.
frontend/packages/framework/src/hooks/index.ts Exports the new hook/constant from the hooks barrel.
frontend/packages/apps/snippets/src/SnippetsListPage.tsx Replaces the inline debounce effect with useDebouncedValue(search.trim()).
frontend/packages/apps/snippets/src/SnippetsListPage.test.tsx Switches to a partial @sep/framework mock so useDebouncedValue runs as real code while keeping the download hook stubbed.
frontend/packages/apps/atw/src/CollectPane.tsx Replaces the inline debounce effect with useDebouncedValue(searchInput.trim()).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@nachodd nachodd added the qa passed Tests for this PR are completed and successful. label Aug 19, 2026
@yyyyyyyan
yyyyyyyan enabled auto-merge (squash) August 19, 2026 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:atw PR touches the atw app slice app:snippets PR touches the snippets app slice frontend qa passed Tests for this PR are completed and successful.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants