mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-15 07:06:11 +00:00
Merge 51798b761b3faf49c64e5438a8eb6223e45cd849 into 1f59e814f90c3f489f48d68262cb1bf640bf6181
This commit is contained in:
commit
3750b9bc8c
@ -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