Split ChatNostrCoordinator into owned components along domain boundaries

The 1,109-line coordinator becomes a 187-line facade wiring three
components, each with its own narrow context protocol:
GeohashSubscriptionManager (384 lines - subscription IDs + relay
lifecycle, the only NostrRelayManager toucher), NostrInboundPipeline
(490 lines - the hot event path, dedup-before-verify ordering preserved
verbatim), and GeoPresenceTracker (192 lines - teleport detection,
sampling LRU, notification cooldowns, now directly tested).

Perf baselines confirm the hot path is unchanged: fresh events
2,131 -> 2,138/sec, duplicates ~1.41M/sec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-06-10 22:28:08 +02:00
co-authored by Claude Fable 5
parent 6091ee83ad
commit 638f3f5005
7 changed files with 1227 additions and 986 deletions
@@ -51,7 +51,7 @@ final class PerformanceBaselineTests: XCTestCase {
// MARK: - 1a. Nostr inbound event handling (fresh events)
/// `ChatNostrCoordinator.handleNostrEvent` for never-seen geo events
/// `NostrInboundPipeline.handleNostrEvent` for never-seen geo events
/// (kind 20000): signature verification, dedup record, presence/nickname
/// bookkeeping, and public-message ingest scheduling.
func testNostrInboundEventHandling_freshEvents() throws {
@@ -68,7 +68,7 @@ final class PerformanceBaselineTests: XCTestCase {
keepAlive.append((context, coordinator))
let start = Date()
for event in events {
coordinator.handleNostrEvent(event)
coordinator.inbound.handleNostrEvent(event)
}
samples.append(Date().timeIntervalSince(start))
XCTAssertEqual(context.processedEventCount, events.count)
@@ -89,7 +89,7 @@ final class PerformanceBaselineTests: XCTestCase {
let coordinator = ChatNostrCoordinator(context: context)
// Pre-warm: every event is now recorded as processed.
for event in events {
coordinator.handleNostrEvent(event)
coordinator.inbound.handleNostrEvent(event)
}
XCTAssertEqual(context.processedEventCount, events.count)
var samples: [TimeInterval] = []
@@ -97,7 +97,7 @@ final class PerformanceBaselineTests: XCTestCase {
measure {
let start = Date()
for event in events {
coordinator.handleNostrEvent(event)
coordinator.inbound.handleNostrEvent(event)
}
samples.append(Date().timeIntervalSince(start))
}
@@ -370,8 +370,9 @@ private struct SeededGenerator: RandomNumberGenerator {
// MARK: - Mock ChatNostrContext
/// Minimal `ChatNostrContext` for benchmarking `ChatNostrCoordinator`
/// without a `ChatViewModel` (mirrors `ChatNostrCoordinatorContextTests`).
/// Minimal `ChatNostrContext` for benchmarking the `ChatNostrCoordinator`
/// stack (`NostrInboundPipeline` in particular) without a `ChatViewModel`
/// (mirrors `ChatNostrCoordinatorContextTests`).
/// Callbacks are cheap dictionary/array operations so the measured cost is
/// the coordinator's own pipeline.
@MainActor