mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 22:25:20 +00:00
Geohash: serialize PoW sends so order matches send order
Two location-channel sends back-to-back only cancelled the previous mining task and started a new one. Cancellation merely *expedites* NIP-13 mining (the target is polled and steps down; it never aborts the send), so the cancelled task still appended + relayed once mining returned. Both tasks ran concurrently and the second (shorter to mine) could finish first, reordering messages in the timeline and on relays. Chain the mining tasks: each geohash send captures the previous send's task, cancels it (to expedite, so delays never stack), and awaits its completion before it echoes and relays. Order is now always send order. The >2s mining cap is preserved: cancellation expedites the awaited task, so a send is never blocked beyond NostrPoW.miningTimeCap. Test: two rapid sends where the first mines longer (larger content) still land in send order for both the local echo and the relayed events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -218,4 +218,35 @@ struct ChatOutgoingCoordinatorContextTests {
|
||||
#expect(context.appendedPublicMessages.count == 1)
|
||||
#expect(context.sentGeohashContexts.count == 1)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func sendMessage_onLocationChannel_serializesRapidSendsInSendOrder() async {
|
||||
let context = MockChatOutgoingContext()
|
||||
let coordinator = ChatOutgoingCoordinator(context: context)
|
||||
let channel = GeohashChannel(level: .city, geohash: "u4pruydq")
|
||||
context.activeChannel = .location(channel)
|
||||
|
||||
// Two back-to-back sends. The first carries much larger content, so
|
||||
// its NIP-13 mining hashes a bigger event per attempt and runs longer
|
||||
// than the second's. Without serialization the second (faster) task
|
||||
// could finish first and reorder both the local timeline and the
|
||||
// relayed events. The coordinator chains the mining tasks — each send
|
||||
// awaits the previous send's task before it echoes and relays — so the
|
||||
// visible order must always match the send order.
|
||||
let first = "first " + String(repeating: "x", count: 4000)
|
||||
let second = "second"
|
||||
coordinator.sendMessage(first)
|
||||
coordinator.sendMessage(second)
|
||||
|
||||
// The stored task is the second send, which awaits the first.
|
||||
await coordinator.geohashMiningTask?.value
|
||||
await drainMainActorTasks()
|
||||
|
||||
// Local echoes land in send order…
|
||||
#expect(context.appendedPublicMessages.map(\.message.content) == [first, second])
|
||||
// …and so do the relayed events (IDs match the echoes 1:1, in order).
|
||||
#expect(context.sentGeohashContexts.count == 2)
|
||||
#expect(context.sentGeohashContexts.map(\.event.id)
|
||||
== context.appendedPublicMessages.map(\.message.id))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user