mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
HapiClient/SSE per docs/api/client-contract/sse.md and the web reference (web/src/hooks/useSSE.ts): - SSELineParser: incremental frame parser (LF/CRLF, multi-line data, comments, retry ignored); per-block ids only — sticky-cursor semantics live in the caller so id-less heartbeats can never blank the cursor. - ReconnectPolicy + SSETimings: 0s first retry, 1s x2 capped at 30s, 300s slow ceiling after 8 attempts, 0-500ms injectable jitter; connect timeout 10s, staleness 90s, foreground-resume 45s, watchdog tick 10s. - SSETransport protocol + URLSessionSSETransport (URLSession.bytes with dedicated config: 300s idle timeout, 7d resource, no waitsForConnectivity). - actor SSEClient: idle/connecting/connected/backoff/suspended machine; handshake-gated connected state surfacing resume ok|gap; per-subscription cursor sent as ?lastEventId, advanced only after consumer yield (at-least-once); one free token-refresh retry per connect cycle on 401; suspend defers retries, resume applies the 45s staleness check; acceptEncodingIdentity escape hatch (gzip streaming verification TODO). - NetworkPathObserving + NWPathObserver: path change while connected is treated as a transport error (immediate reconnect). - swift-testing suite on a fake transport + manual clock: parser edge cases, exact backoff schedule + seeded jitter bounds, handshake gating, ok/gap verdicts, cursor replay + isolation, heartbeat vs watchdog, 90s staleness, connect timeout, suspend/resume, event ordering, unknown-type passthrough, 401 bypass cap, path-change reconnect.
34 lines
1.2 KiB
Swift
34 lines
1.2 KiB
Swift
import Dispatch
|
|
import Foundation
|
|
|
|
/// Minimal time source used by `SSEClient` for the connect timeout, the
|
|
/// staleness watchdog, and backoff sleeps.
|
|
///
|
|
/// Deliberately NOT the stdlib `Clock` protocol: tests need a manually
|
|
/// advanced clock whose `sleep` parks on a continuation until test code
|
|
/// moves time forward, and the stdlib protocol's associated types and
|
|
/// tolerance machinery buy nothing here.
|
|
public protocol SSEClock: Sendable {
|
|
/// Current time in milliseconds on a monotonically increasing scale.
|
|
/// Only ever used for differences; the origin is arbitrary.
|
|
func nowMs() -> Int
|
|
/// Suspends for `ms`. Must throw `CancellationError` promptly when the
|
|
/// surrounding task is cancelled — the client relies on it to abort
|
|
/// watchdog ticks and pending backoff sleeps.
|
|
func sleep(ms: Int) async throws
|
|
}
|
|
|
|
/// Production clock: monotonic uptime + `Task.sleep`.
|
|
public struct SystemSSEClock: SSEClock {
|
|
public init() {}
|
|
|
|
public func nowMs() -> Int {
|
|
Int(DispatchTime.now().uptimeNanoseconds / 1_000_000)
|
|
}
|
|
|
|
public func sleep(ms: Int) async throws {
|
|
guard ms > 0 else { return }
|
|
try await Task.sleep(nanoseconds: UInt64(ms) * 1_000_000)
|
|
}
|
|
}
|