mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-22 07:16:03 +00:00
fix: evict fully-aged-out geohashes from activity tracker
messageTimes/lastMessages kept an entry for every geohash ever seen — prune() only emptied a geohash's timestamp array, never dropped the dictionary entry itself. roaming across many regions over a long session leaked one entry per distinct geohash for the life of the process. sweep other geohashes on each recorded message and drop the ones that have fully aged out.
This commit is contained in:
parent
1f59e814f9
commit
a5bb31e0fd
@ -41,6 +41,12 @@ final class GeohashChatActivityTracker: ObservableObject {
|
||||
private var lastMessages: [String: GeohashChatPreview] = [:]
|
||||
private let now: () -> Date
|
||||
|
||||
#if DEBUG
|
||||
/// Number of distinct geohashes currently held in memory; exposed only
|
||||
/// for regression tests verifying stale entries get evicted.
|
||||
var _trackedGeohashCountForTesting: Int { messageTimes.count }
|
||||
#endif
|
||||
|
||||
init(
|
||||
window: TimeInterval = TransportConfig.uiGeohashChatActivityWindowSeconds,
|
||||
now: @escaping () -> Date = { Date() }
|
||||
@ -68,6 +74,15 @@ final class GeohashChatActivityTracker: ObservableObject {
|
||||
} else {
|
||||
lastMessages[gh] = GeohashChatPreview(senderName: senderName, content: content, timestamp: clamped)
|
||||
}
|
||||
|
||||
// A geohash's timestamp array only ever gets pruned down to empty —
|
||||
// its dictionary entry (and the paired lastMessages entry) is never
|
||||
// dropped, so roaming across many regions over a long session leaks
|
||||
// one entry per distinct geohash ever observed. Sweep other geohashes
|
||||
// here so activity that has fully aged out gets evicted instead of
|
||||
// sitting in memory for the rest of the process's lifetime.
|
||||
pruneStaleGeohashes(except: gh)
|
||||
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
@ -113,4 +128,15 @@ final class GeohashChatActivityTracker: ObservableObject {
|
||||
let cutoff = now().addingTimeInterval(-window)
|
||||
return times.filter { $0 >= cutoff }
|
||||
}
|
||||
|
||||
/// Drops any tracked geohash (other than `keep`) whose messages have all
|
||||
/// aged out of the window, so the maps stay bounded to recently-active
|
||||
/// geohashes instead of growing for the life of the process.
|
||||
private func pruneStaleGeohashes(except keep: String) {
|
||||
for gh in Array(messageTimes.keys) where gh != keep {
|
||||
guard prune(messageTimes[gh] ?? []).isEmpty else { continue }
|
||||
messageTimes.removeValue(forKey: gh)
|
||||
lastMessages.removeValue(forKey: gh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,4 +110,32 @@ struct GeohashChatActivityTrackerTests {
|
||||
#expect(tracker.messageCount(for: "9q8yy") == 0)
|
||||
#expect(tracker.mostActiveConversation(among: [channel("9q8yy", .city)]) == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func staleGeohashesAreEvictedNotJustEmptied() {
|
||||
let (tracker, advance) = makeTracker(window: 900)
|
||||
tracker.recordChatMessage(geohash: "9q8yy", senderName: "a#1111", content: "old region", timestamp: baseDate)
|
||||
#expect(tracker._trackedGeohashCountForTesting == 1)
|
||||
|
||||
// Move well past the window, then record activity in a different
|
||||
// geohash — this should sweep the now-stale "9q8yy" entry out of
|
||||
// memory instead of leaving an empty-but-present dictionary entry.
|
||||
advance(baseDate.addingTimeInterval(901))
|
||||
tracker.recordChatMessage(geohash: "9r1zz", senderName: "b#2222", content: "new region", timestamp: baseDate.addingTimeInterval(901))
|
||||
|
||||
#expect(tracker._trackedGeohashCountForTesting == 1)
|
||||
#expect(tracker.messageCount(for: "9q8yy") == 0)
|
||||
#expect(tracker.messageCount(for: "9r1zz") == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func activeGeohashSurvivesSweepEvenWhenMomentarilyEmpty() {
|
||||
// The geohash currently being recorded to is exempt from the sweep,
|
||||
// so recording its own first message never evicts itself.
|
||||
let (tracker, _) = makeTracker(window: 900)
|
||||
tracker.recordChatMessage(geohash: "9q8yy", senderName: "a#1111", content: "hi", timestamp: baseDate)
|
||||
|
||||
#expect(tracker._trackedGeohashCountForTesting == 1)
|
||||
#expect(tracker.messageCount(for: "9q8yy") == 1)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user