From 60ce3b2eb28d33db29345e61389bc5be07f3b1e8 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 10 May 2024 15:54:25 -0700 Subject: [PATCH] Fix an issue in `Debounce` that added up debounce durations If calls from `Debouncer` were scheduled every 0.5s but the debounce duration was 1s, we would always schedule the pending task and schedule a new one, never actually committing one. Debounce at most the debounce duration from the initially scheduled call. --- Sources/SKCore/Debouncer.swift | 19 +++++++++++-------- .../SemanticIndex/SemanticIndexManager.swift | 1 - 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/SKCore/Debouncer.swift b/Sources/SKCore/Debouncer.swift index 6cb596ad..8ee7cca4 100644 --- a/Sources/SKCore/Debouncer.swift +++ b/Sources/SKCore/Debouncer.swift @@ -33,14 +33,15 @@ public actor Debouncer { private let makeCall: (Parameter) async -> Void /// In the time between the call to `scheduleCall` and the call actually being committed (ie. in the time that the - /// call can be debounced), the task that would commit the call (unless cancelled) and the parameter with which this - /// call should be made. - private var inProgressData: (Parameter, Task)? + /// call can be debounced), the task that would commit the call (unless cancelled), the parameter with which this + /// call should be made and the time at which the call should be made. Keeping track of the time ensures that we don't + /// indefinitely debounce if a new `scheduleCall` is made every 0.4s but we debounce for 0.5s. + private var inProgressData: (Parameter, ContinuousClock.Instant, Task)? public init( debounceDuration: Duration, combineResults: @escaping (Parameter, Parameter) -> Parameter, - _ makeCall: @escaping (Parameter) async -> Void + _ makeCall: @Sendable @escaping (Parameter) async -> Void ) { self.debounceDuration = debounceDuration self.combineParameters = combineResults @@ -52,13 +53,15 @@ public actor Debouncer { /// `debounceDuration` after the second `scheduleCall` call. public func scheduleCall(_ parameter: Parameter) { var parameter = parameter - if let (inProgressParameter, inProgressTask) = inProgressData { + var targetDate = ContinuousClock.now + debounceDuration + if let (inProgressParameter, inProgressTargetDate, inProgressTask) = inProgressData { inProgressTask.cancel() parameter = combineParameters(inProgressParameter, parameter) + targetDate = inProgressTargetDate } let task = Task { do { - try await Task.sleep(for: debounceDuration) + try await Task.sleep(until: targetDate) try Task.checkCancellation() } catch { return @@ -66,12 +69,12 @@ public actor Debouncer { inProgressData = nil await makeCall(parameter) } - inProgressData = (parameter, task) + inProgressData = (parameter, ContinuousClock.now + debounceDuration, task) } } extension Debouncer { - public init(debounceDuration: Duration, _ makeCall: @escaping () async -> Void) { + public init(debounceDuration: Duration, _ makeCall: @Sendable @escaping () async -> Void) { self.init(debounceDuration: debounceDuration, combineResults: { _, _ in }, makeCall) } diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index ef576007..09c6ae3d 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -414,7 +414,6 @@ public final actor SemanticIndexManager { /// /// If file's target is known to be up-to-date, this returns almost immediately. public func prepareFileForEditorFunctionality(_ uri: DocumentURI) async { - // Should be kept in sync with `schedulePreparationForEditorFunctionality`. guard let target = await buildSystemManager.canonicalConfiguredTarget(for: uri) else { return }