Files
web-terminal/ios/Packages/WireProtocol/Sources/WireProtocol/TimelineEvent.swift
Yaojia Wang cbaa08daba feat(ios): W0 scaffold + day-1 spike + WireProtocol frozen contract + TestSupport doubles
T-iOS-1: ios/ XcodeGen project (iOS 17, Swift 6 strict concurrency, ATS per PLAN §5.2), 5 SPM package shells, CI skeleton
T-iOS-2: Origin spike vs real server — URLSessionWebSocketTask custom Origin CONFIRMED (no Starscream); 16MiB replay + EMSGSIZE(40) errno correction written back to plan
T-iOS-3: WireProtocol frozen contract, 59 tests, 100% line coverage, cross-impl vectors vs src/protocol.ts via tsx
T-iOS-4: FakeTransport/FakeClock/FakeHTTPTransport doubles
Verify: independent agent re-ran all acceptance — 6/6 PASS
2026-07-04 21:19:30 +02:00

54 lines
2.1 KiB
Swift

import Foundation
/// One activity-timeline entry from `GET /live-sessions/:id/events` (frozen
/// contract, plan §3.1; mirrors src/types.ts:428-433). The server derives
/// `class` semantically (src/types.ts:423: tool/waiting/done/stuck/user) the
/// client never re-derives it from hook names. `class` stays a raw `String` so
/// the SHAPE decodes even for future/unknown classes; consumers drop unknowns
/// (use `decodeList`, which does exactly that).
public struct TimelineEvent: Sendable, Equatable, Decodable {
/// Server ingest timestamp (ms since epoch).
public let at: Int
/// Server-derived semantic class; see `knownClasses`.
public let `class`: String
/// Sanitized tool name (server-side: 200 chars, control chars stripped).
public let toolName: String?
/// Server-derived human phrase ("ran Bash", "edited 3 files").
public let label: String
/// The server's `TimelineClass` union (src/types.ts:423).
public static let knownClasses: Set<String> = ["tool", "waiting", "done", "stuck", "user"]
public init(at: Int, class className: String, toolName: String?, label: String) {
self.at = at
self.`class` = className
self.toolName = toolName
self.label = label
}
/// True when `class` is one the server currently emits.
public var hasKnownClass: Bool {
Self.knownClasses.contains(`class`)
}
/// Decode a `/events` response body. NEVER throws: the server is an
/// untrusted input source malformed entries and unknown-class entries are
/// silently dropped; a non-array body yields `[]`.
public static func decodeList(from data: Data) -> [TimelineEvent] {
guard let entries = try? JSONDecoder().decode([LossyEntry].self, from: data) else {
return []
}
return entries.compactMap(\.value).filter(\.hasKnownClass)
}
}
/// Per-element tolerance shim: a malformed element becomes `nil` instead of
/// failing the whole array decode.
private struct LossyEntry: Decodable {
let value: TimelineEvent?
init(from decoder: any Decoder) {
value = try? TimelineEvent(from: decoder)
}
}