Skip to content

Commit 4fd6166

Browse files
grdsdevclaude
andcommitted
fix(realtime): implement event buffering for URLSessionWebSocket
Add event buffering mechanism to prevent message loss when onEvent callback is not yet attached. Events are buffered and replayed when callback is set. - Add eventBuffer to MutableState to store incoming events - Modify _trigger to buffer events when onEvent is nil - Update onEvent setter to replay buffered events when callback attached - Implement 100-event buffer limit to prevent memory issues - Clear buffer on connection close for proper cleanup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent c3c2787 commit 4fd6166

File tree

1 file changed

+33
-2
lines changed

1 file changed

+33
-2
lines changed

Sources/Realtime/WebSocket/URLSessionWebSocket.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ final class URLSessionWebSocket: WebSocket {
129129
var isClosed = false
130130
/// Callback for handling WebSocket events.
131131
var onEvent: (@Sendable (WebSocketEvent) -> Void)?
132+
/// Buffer for events received before onEvent callback is attached.
133+
var eventBuffer: [WebSocketEvent] = []
132134
/// The close code received when connection was closed.
133135
var closeCode: Int?
134136
/// The close reason received when connection was closed.
@@ -271,24 +273,53 @@ final class URLSessionWebSocket: WebSocket {
271273
/// - `.binary(Data)`: Binary messages received from the peer
272274
/// - `.close(code: Int?, reason: String)`: Connection closed events
273275
///
276+
/// When setting this callback, any buffered events received before the callback
277+
/// was attached will be replayed immediately in the order they were received.
278+
///
274279
/// The callback is called on an arbitrary queue and should be thread-safe.
275280
var onEvent: (@Sendable (WebSocketEvent) -> Void)? {
276281
get { mutableState.value.onEvent }
277-
set { mutableState.withValue { $0.onEvent = newValue } }
282+
set {
283+
mutableState.withValue { state in
284+
state.onEvent = newValue
285+
286+
// Replay buffered events when callback is attached
287+
if let onEvent = newValue, !state.eventBuffer.isEmpty {
288+
let bufferedEvents = state.eventBuffer
289+
state.eventBuffer.removeAll()
290+
291+
for event in bufferedEvents {
292+
onEvent(event)
293+
}
294+
}
295+
}
296+
}
278297
}
279298

280299
/// Triggers a WebSocket event and updates internal state if needed.
281300
/// - Parameter event: The event to trigger.
282301
private func _trigger(_ event: WebSocketEvent) {
283302
mutableState.withValue {
284-
$0.onEvent?(event)
303+
if let onEvent = $0.onEvent {
304+
// Deliver event immediately if callback is available
305+
onEvent(event)
306+
} else {
307+
// Buffer event if no callback is attached yet
308+
// Limit buffer size to prevent memory issues (keep last 100 events)
309+
$0.eventBuffer.append(event)
310+
if $0.eventBuffer.count > 100 {
311+
$0.eventBuffer.removeFirst()
312+
}
313+
}
285314

286315
// Update state when connection closes
287316
if case .close(let code, let reason) = event {
288317
$0.onEvent = nil
289318
$0.isClosed = true
290319
$0.closeCode = code
291320
$0.closeReason = reason
321+
// Clear buffer when connection closes
322+
$0.eventBuffer.removeAll()
292323
}
293324
}
294325
}

0 commit comments

Comments
 (0)