feat: async command sources with debounce + abort - #31
Open
Priyans-hu wants to merge 4 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the long-standing gap that
isLoadingwas alwaysfalseand there was no way to register async data sources (Linear/GitHub/API search). Every comparable lib (kbar, ninja-keys, Raycast) supports this.What changed
config.asyncSourcesaccepting{ id, trigger?, load, debounceMs? }[]loadreceives anAbortSignaland is cancelled on new queriesisLoadingnow reflects in-flight sourcesasyncErrors: Record<sourceId, Error>returned fromuseCommandPaletteid(async wins on collision)Tests
+19 tests (196 -> 215 passing). Covers:
Hook tests use
vi.useFakeTimers()+ microtask flushing to drive the debounce/promise pipeline deterministically.Migration
None —
asyncSourcesis optional.asyncErrorsis added as an optional field onCommandPaletteStateso existing consumers compile unchanged.Design notes
src/core/async-sources.ts(framework-agnostic and unit-testable) so the React hook stays focused on effect plumbing.AbortControllermap in auseRef; each query change aborts all prior controllers before scheduling new debounced timers.console.errorand stored inasyncErrors[id]. Successful runs clear the prior error for that source.