Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions GraphcodeKit/Sources/GraphStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,11 @@ public actor GraphStore {
// MARK: - Connections

public func addConnection(id: UUID, fileDescriptor: Int32) {
// Joining a project registers the connection here too, so ensure its outbound half
// the same way the registry does. Without this a store could bind to a channel left
// dead on a recycled descriptor number and drop the client as disconnected on the
// snapshot it was joining for.
OutboundChannels.open(fileDescriptor)
connections[id] = fileDescriptor
send(.graphChanged(graph), to: id)
}
Expand Down Expand Up @@ -3207,10 +3212,26 @@ public actor GraphStore {
private func send(_ event: DaemonEvent, to connectionID: UUID) {
guard let fileDescriptor = connections[connectionID] else { return }
guard let data = try? JSONEncoder().encode(event) else { return }
guard (try? FramedMessageIO.writeFrame(data, to: fileDescriptor)) != nil else {
// The write failed — most likely the client already disconnected. Drop it here
// rather than waiting for the read loop to notice, so a dead connection can't
// accumulate failed broadcast attempts.
// Queued, never written here: this runs on the `GraphStore` actor, and a
// `graphChanged` frame is far larger than a socket's send buffer, so writing it
// inline blocked the actor for as long as the slowest client took to read
// (issue #288). A snapshot still waiting to go out is replaced by a newer one rather
// than queued behind it — the event carries the whole graph, so the older one has
// nothing left to say.
//
// Keyed per graph, never on the event name alone: one connection joins as many
// projects as it likes, and every project's store writes to that one socket. A shared
// key made the newest snapshot supersede a *different* project's undelivered one, so
// a client that had just joined two projects silently never received the first — its
// loops simply never appeared.
let supersedingKey: String? = {
if case .graphChanged(let changed) = event { return "graphChanged:\(changed.id)" }
return nil
}()
guard OutboundChannels.send(data, to: fileDescriptor, supersedingKey: supersedingKey) else {
// No live channel — the client already disconnected. Drop it here rather than
// waiting for the read loop to notice, so a dead connection can't accumulate
// failed broadcast attempts.
connections.removeValue(forKey: connectionID)
return
}
Expand Down
Loading
Loading