feat: make wait assignable so one instance can vary its delay - #9
Closed
xsahil03x wants to merge 2 commits into
Closed
feat: make wait assignable so one instance can vary its delay#9xsahil03x wants to merge 2 commits into
xsahil03x wants to merge 2 commits into
Conversation
`wait` was fixed at construction, so varying the delay meant one Debounce
per delay. stream_chat_flutter's SearchDebouncer does exactly that — two
instances cancelled against each other, with isPending OR-ing the pair —
and says so in a comment.
`waitBuilder` receives the same arguments as the debounced function and
returns the wait for that call:
debounce(
onSearch,
const Duration(milliseconds: 300),
waitBuilder: (args, _) => policy.delayFor(args!.first! as String),
);
Shrinking the wait has to re-arm the pending timer, which was armed for
the previous, longer one — otherwise the call fires late. Growing it
already self-corrected through the existing restart path.
Throttle keeps maxWait equal to wait. Since maxWait is always raised to
at least wait, passing zero pins it to whatever the builder last
returned; leaving it at the constructed wait made a throttle with a
shrinking wait fire half as often as asked.
Behaviour is unchanged when no waitBuilder is given: the resolver returns
early and every existing test passes untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #9 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 4 4
Lines 100 116 +16
=========================================
+ Hits 100 116 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The waitBuilder callback received the same List<Object?>/Map<Symbol,
Object?> that Function.apply does, so every use site had to null-assert
and cast its way back to the type it already knew:
waitBuilder: (args, _) => (args!.first! as int).toDuration()
That is the untyped call path leaking into a brand new API. A typed
builder — (String query) => ... — needs no casts at all, but it only
exists once Debounce itself is generic, so it belongs after that work
rather than before it.
The caller already holds the value in typed form, so nothing needs to
hand it back to them:
debouncedSearch.wait = query.length <= 2 ? 500.ms : 300.ms;
debouncedSearch([query]);
Smaller surface too: one property, readable as well as assignable,
instead of a parameter on four constructors and factories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Why
waitis fixed at construction, so varying the delay means oneDebounceper delay.stream_chat_flutter'sSearchDebouncerdoes exactly that, and documents it:Two instances, manually cancelled against each other, with
isPendingOR-ing the pair. Swift solves the same feature withSearchDebouncePolicy.interval(forQueryLength:).What
waitbecomes a readable and assignable property onDebounceandThrottle. A new value takes effect from the next call.SearchDebouncercollapses to one instance with no cross-cancellation:Why not a
waitBuildercallbackThat was the first version of this PR, and it was wrong. The callback received the same
List<Object?>?/Map<Symbol, Object?>?thatFunction.applyuses, so every use site had to null-assert and cast its way back to a type it already knew:That is the untyped call path leaking straight into a brand new API. A typed builder —
(String query) => ...— needs no casts, but it only exists onceDebounceitself is generic, so it belongs after that work rather than before it.The caller already holds the value in typed form, so there is no reason to hand it back to them. The property is also a smaller surface: one member, readable as well as assignable, rather than a parameter threaded through two constructors, two extension methods and two top-level functions.
Two behaviours that needed care
A shrinking wait has to re-arm the pending timer. It was armed for the previous, longer wait, so the call would fire late — 500ms after the first call rather than 200ms after the second. Growing the wait already self-corrected through the existing restart path in
_timerExpired, so only the shrink case needed the fix; the grow test documents the behaviour rather than guarding new code.Throttlemust keepmaxWaitequal towait.maxWaitis always raised to at leastwait, so passingDuration.zeropins it to whateverwaitcurrently is. Left at the constructed value, a throttle whose wait was lowered fired half as often as asked — measured as 2 invocations instead of 4 over a 200ms tight loop.Compatibility
Additive and non-breaking. Nothing changes unless
waitis assigned — all 39 pre-existing tests pass untouched.Verification
dart format --set-exit-if-changed .,dart analyze --fatal-infos ., 46 tests under randomized ordering, coverage 100% (116/116),dart pub publish --dry-runclean.Each new behaviour is mutation-checked — removing the timer re-arm fails only the shrink test, and reverting the
maxWaitpin fails only the throttle test.🤖 Generated with Claude Code