Skip to content

Commit 2b0ab2c

Browse files
ajaysubraclaude
andcommitted
fix: address Cursor BugBot review findings on token-bucket governor
- Reword `flushDepth` doc comment to describe behavior without cross-repo reference (Android SDK mention removed per org policy) - Add `resetFlushTokenBucket()` helper on KlaviyoState that restores availableFlushTokens to capacity and clears lastFlushTokenRefill - Call `resetFlushTokenBucket()` on the company-switch path of `.initialize` so a depleted bucket from the previous API key cannot throttle flushes for the incoming company - Add unit test `test_initialize_withNewApiKey_resetsBucketToFull` verifying the bucket is restored to full after switching API keys Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 240a5d4 commit 2b0ab2c

3 files changed

Lines changed: 47 additions & 3 deletions

File tree

Sources/KlaviyoSwift/StateManagement/KlaviyoState+FlushTokenBucket.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
import Foundation
1414

1515
extension KlaviyoState {
16+
/// Resets the token bucket to full capacity with no recorded refill timestamp,
17+
/// reproducing the cold-launch state. Call this whenever a new API key is set so
18+
/// the bucket from the previous company does not throttle the incoming company's
19+
/// first flush cycle.
20+
mutating func resetFlushTokenBucket() {
21+
availableFlushTokens = StateManagementConstants.flushTokenBucketCapacity
22+
lastFlushTokenRefill = nil
23+
}
24+
1625
/// `true` when the queue has grown large enough to warrant an early flush attempt
1726
/// rather than waiting for the next flush-interval tick. Suppressed when the
1827
/// `flushInterval` is non-finite (i.e. offline) so we don't kick off doomed requests.

Sources/KlaviyoSwift/StateManagement/StateManagement.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ enum StateManagementConstants {
3333
static let flushTokenBucketCapacity = 5.0
3434

3535
/// Queue depth that triggers an early flush attempt instead of waiting for the next
36-
/// flush-interval tick. Mirrors the Android SDK's batch flush depth so the burst
37-
/// behavior is consistent across platforms. The token bucket still gates whether the
38-
/// early flush actually proceeds.
36+
/// flush-interval tick. When the queue reaches this size the SDK schedules an immediate
37+
/// flush to drain large bursts quickly; the token bucket still gates whether the flush
38+
/// actually proceeds, so the long-term rate is preserved.
3939
static let flushDepth = 25
4040
}
4141

@@ -199,6 +199,9 @@ struct KlaviyoReducer: ReducerProtocol {
199199
}
200200
state.apiKey = apiKey
201201
state.reset()
202+
// Restore the token bucket to full capacity so the incoming company's
203+
// first flush is not throttled by activity from the previous company.
204+
state.resetFlushTokenBucket()
202205
}
203206
guard case .uninitialized = state.initalizationState else {
204207
return .none

Tests/KlaviyoSwiftTests/FlushTokenBucketTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,36 @@ final class FlushTokenBucketTests: XCTestCase {
147147
await store.send(.enqueueAggregateEvent(Data("agg".utf8)))
148148
await store.receive(.flushQueue)
149149
}
150+
151+
// MARK: - token bucket reset on company switch
152+
153+
/// Verifies that re-initializing with a *different* API key resets the token bucket to
154+
/// full capacity, so a depleted bucket from the previous company cannot throttle the
155+
/// incoming company's first flush cycle.
156+
///
157+
/// When `.initialize` is dispatched against an already-initialized state with a new key,
158+
/// the reducer resets profile data and restores the bucket, then returns `.none` (it
159+
/// cannot transition to `.initializing` because `initalizationState` is no longer
160+
/// `.uninitialized`). The state is verified synchronously via the `send` expectation
161+
/// before any async work fires.
162+
@MainActor
163+
func test_initialize_withNewApiKey_resetsBucketToFull() async {
164+
// Start with an already-initialized state whose bucket is fully depleted.
165+
var initialState = INITIALIZED_TEST_STATE()
166+
initialState.availableFlushTokens = 0
167+
initialState.lastFlushTokenRefill = environment.date()
168+
169+
let store = TestStore(initialState: initialState, reducer: KlaviyoReducer())
170+
store.exhaustivity = .off
171+
172+
let newApiKey = "new-company-key"
173+
await store.send(.initialize(newApiKey)) {
174+
// The bucket must be restored to full capacity on a company switch.
175+
$0.availableFlushTokens = StateManagementConstants.flushTokenBucketCapacity
176+
$0.lastFlushTokenRefill = nil
177+
$0.apiKey = newApiKey
178+
// initalizationState stays .initialized — the guard in .initialize exits early
179+
// because the state is not .uninitialized after the company-switch branch runs.
180+
}
181+
}
150182
}

0 commit comments

Comments
 (0)