Skip to content

feat: async command sources with debounce + abort - #31

Open
Priyans-hu wants to merge 4 commits into
mainfrom
feat/async-sources
Open

feat: async command sources with debounce + abort#31
Priyans-hu wants to merge 4 commits into
mainfrom
feat/async-sources

Conversation

@Priyans-hu

Copy link
Copy Markdown
Owner

Closes the long-standing gap that isLoading was always false and there was no way to register async data sources (Linear/GitHub/API search). Every comparable lib (kbar, ninja-keys, Raycast) supports this.

What changed

  • New config.asyncSources accepting { id, trigger?, load, debounceMs? }[]
  • load receives an AbortSignal and is cancelled on new queries
  • isLoading now reflects in-flight sources
  • Errors per source are isolated; one failing source doesn't break the palette
  • New asyncErrors: Record<sourceId, Error> returned from useCommandPalette
  • Async items are merged with statically registered commands by id (async wins on collision)
  • Async sources only fire at root depth — nested menus continue to use inline children

Tests

+19 tests (196 -> 215 passing). Covers:

  • single source, multiple sources, error isolation
  • abort on new query, trigger predicate gating, debounce coalescing
  • empty-query no-op behaviour
  • core helpers (defaultTrigger, shouldRunSource, mergeAsyncCommands, flattenAsyncItems)

Hook tests use vi.useFakeTimers() + microtask flushing to drive the debounce/promise pipeline deterministically.

Migration

None — asyncSources is optional. asyncErrors is added as an optional field on CommandPaletteState so existing consumers compile unchanged.

Design notes

  • Merge / dedupe lives in src/core/async-sources.ts (framework-agnostic and unit-testable) so the React hook stays focused on effect plumbing.
  • Per-source AbortController map in a useRef; each query change aborts all prior controllers before scheduling new debounced timers.
  • Errors are captured + logged via console.error and stored in asyncErrors[id]. Successful runs clear the prior error for that source.

Priyans-hu added 4 commits May 4, 2026 01:02
Introduces the AsyncSource interface (id, optional trigger predicate,
load fn receiving an AbortSignal, debounceMs) and the asyncSources field
on CommandEngineConfig. Adds asyncErrors to CommandPaletteState.

Also exposes async-sources core helpers (defaultTrigger, shouldRunSource,
mergeAsyncCommands, flattenAsyncItems, DEFAULT_ASYNC_DEBOUNCE_MS) for
reuse by the React hook and downstream callers.
Drives config.asyncSources from useCommandPalette: each source is debounced
per its debounceMs (default 200), invoked with an AbortSignal that is
aborted on the next query change, and its results are merged into
activeCommands by id (async wins on conflict).

isLoading now reflects whether any source is in flight. Errors per source
are captured in asyncErrors keyed by source id and logged, so a failing
source does not break the palette. Sources are only fired at root depth;
nested menus continue to use their inline children.
Hook tests (tests/react/async-sources.test.tsx):
- single async source returns items, isLoading toggles correctly
- two sources contribute results in parallel
- failing source isolates its error in asyncErrors, others still resolve
- new query while a request is in flight aborts the prior signal
- trigger predicate gates loading (e.g., only fires for queries starting with '>')
- debounce coalesces rapid keystrokes into a single load
- empty query with no trigger override skips loading entirely

Core tests (tests/core/async-sources.test.ts):
- defaultTrigger / shouldRunSource gating
- mergeAsyncCommands appends and dedupes; async wins on collision
- flattenAsyncItems folds N sources, keeping first occurrence on dedupe

Uses vi.useFakeTimers() for debounce/abort timing and microtask flushing
to drive promise chains while timers are stubbed.
README:
- new feature row in the comparison table
- dedicated 'Async / Dynamic Command Sources' section with a
  searchLinearIssues example showing trigger, debounceMs, and AbortSignal
- AsyncSource added to the type-safety import block
- isLoading and asyncErrors documented in the hook return summary

Changeset: minor bump describing the new config shape and behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant