2026-01-08 19:16:32 +00:00
//
// L o c a l S e r v e r . s w i f t
// i r i s
//
// C r e a t e d b y C o d e x .
//
import Foundation
import Network
final class LocalServer : ObservableObject {
@ Published private ( set ) var isRunning = false
@ Published private ( set ) var port : Int
@ Published private ( set ) var sseClientCount = 0
@ Published private ( set ) var lastWinnerTitle = " All Quiet "
@ Published private ( set ) var lastWinnerSubtitle = " No urgent updates "
@ Published private ( set ) var lastBroadcastAt : Date ? = nil
@ Published private ( set ) var listenerState : String = " idle "
@ Published private ( set ) var listenerError : String ? = nil
@ Published private ( set ) var lastConnectionAt : Date ? = nil
@ Published private ( set ) var localAddresses : [ String ] = [ ]
private let queue = DispatchQueue ( label : " iris.localserver.queue " )
private var listener : NWListener ?
private var browser : NWBrowser ?
private var startDate = Date ( )
2026-01-10 00:25:36 +00:00
private var currentEnvelope : FeedEnvelope
2026-01-08 19:16:32 +00:00
private var heartbeatTimer : DispatchSourceTimer ?
private var addressTimer : DispatchSourceTimer ?
private var requestBuffers : [ ObjectIdentifier : Data ] = [ : ]
private var clients : [ ObjectIdentifier : SSEClient ] = [ : ]
init ( port : Int = 8765 ) {
self . port = port
2026-01-10 00:25:36 +00:00
self . currentEnvelope = FeedEnvelope . allQuiet ( now : Int ( Date ( ) . timeIntervalSince1970 ) , reason : " no_feed " , source : " server " )
2026-01-08 19:16:32 +00:00
}
var testURL : String {
" http://172.20.10.1: \( port ) /v1/stream "
}
func start ( ) {
guard listener = = nil else { return }
let parameters = NWParameters . tcp
do {
let portValue = NWEndpoint . Port ( rawValue : UInt16 ( port ) ) ? ? 8765
let listener = try NWListener ( using : parameters , on : portValue )
listener . newConnectionHandler = { [ weak self ] connection in
self ? . handleNewConnection ( connection )
}
listener . stateUpdateHandler = { [ weak self ] state in
DispatchQueue . main . async {
self ? . listenerState = " \( state ) "
if case . failed ( let error ) = state {
self ? . listenerError = " \( error ) "
}
self ? . isRunning = ( state = = . ready )
}
}
self . listener = listener
self . startDate = Date ( )
listener . start ( queue : queue )
startHeartbeat ( )
startLocalNetworkPrompt ( )
startAddressUpdates ( )
} catch {
DispatchQueue . main . async {
self . isRunning = false
self . listenerState = " failed "
self . listenerError = " \( error ) "
}
}
}
func stop ( ) {
listener ? . cancel ( )
listener = nil
stopLocalNetworkPrompt ( )
stopHeartbeat ( )
stopAddressUpdates ( )
closeAllClients ( )
DispatchQueue . main . async {
self . isRunning = false
}
}
2026-01-10 00:25:36 +00:00
func broadcastFeed ( _ envelope : FeedEnvelope ) {
currentEnvelope = envelope
let winner = envelope . winnerItem ( )
2026-01-08 19:16:32 +00:00
DispatchQueue . main . async {
2026-01-10 00:25:36 +00:00
self . lastWinnerTitle = winner ? . title ? ? " All Quiet "
self . lastWinnerSubtitle = winner ? . subtitle ? ? " No urgent updates "
2026-01-08 19:16:32 +00:00
self . lastBroadcastAt = Date ( )
}
2026-01-10 00:25:36 +00:00
let data = sseEvent ( name : " feed " , payload : jsonLine ( from : envelope ) )
2026-01-08 19:16:32 +00:00
broadcast ( data : data )
}
private func handleNewConnection ( _ connection : NWConnection ) {
DispatchQueue . main . async {
self . lastConnectionAt = Date ( )
}
connection . stateUpdateHandler = { [ weak self ] state in
if case . failed = state {
self ? . removeClient ( for : connection )
} else if case . cancelled = state {
self ? . removeClient ( for : connection )
}
}
connection . start ( queue : queue )
receiveRequest ( on : connection )
}
private func receiveRequest ( on connection : NWConnection ) {
connection . receive ( minimumIncompleteLength : 1 , maximumLength : 4096 ) { [ weak self ] data , _ , isComplete , error in
guard let self = self else { return }
if let data = data , ! data . isEmpty {
let key = ObjectIdentifier ( connection )
var buffer = self . requestBuffers [ key ] ? ? Data ( )
buffer . append ( data )
self . requestBuffers [ key ] = buffer
if let requestLine = self . parseRequestLine ( from : buffer ) {
self . requestBuffers [ key ] = nil
self . handleRequest ( requestLine : requestLine , connection : connection )
return
}
}
if isComplete || error != nil {
self . requestBuffers [ ObjectIdentifier ( connection ) ] = nil
self . removeClient ( for : connection )
connection . cancel ( )
return
}
self . receiveRequest ( on : connection )
}
}
private func parseRequestLine ( from data : Data ) -> String ? {
guard let string = String ( data : data , encoding : . utf8 ) else { return nil }
guard let range = string . range ( of : " \r \n " ) else { return nil }
return String ( string [ . . < range . lowerBound ] )
}
private func handleRequest ( requestLine : String , connection : NWConnection ) {
let parts = requestLine . split ( separator : " " )
guard parts . count >= 2 else {
sendResponse ( status : " 400 Bad Request " , body : " {} " , contentType : " application/json " , connection : connection )
return
}
let method = parts [ 0 ]
let path = String ( parts [ 1 ] )
guard method = = " GET " else {
sendResponse ( status : " 405 Method Not Allowed " , body : " {} " , contentType : " application/json " , connection : connection )
return
}
switch path {
case " /v1/health " :
let uptime = Int ( Date ( ) . timeIntervalSince ( startDate ) )
let body = " { \" ok \" :true, \" uptime_sec \" : \( uptime ) } "
sendResponse ( status : " 200 OK " , body : body , contentType : " application/json " , connection : connection )
case " /v1/winner " :
let body = jsonLine ( from : currentEnvelope )
sendResponse ( status : " 200 OK " , body : body , contentType : " application/json " , connection : connection )
case " /v1/stream " :
startSSE ( connection )
default :
sendResponse ( status : " 404 Not Found " , body : " {} " , contentType : " application/json " , connection : connection )
}
}
private func sendResponse ( status : String , body : String , contentType : String , connection : NWConnection ) {
let response = " " "
HTTP / 1.1 \ ( status ) \ r \ n \
Content - Type : \ ( contentType ) \ r \ n \
Content - Length : \ ( body . utf8 . count ) \ r \ n \
Connection : close \ r \ n \
\ r \ n \
\ ( body )
" " "
connection . send ( content : response . data ( using : . utf8 ) , completion : . contentProcessed { _ in
connection . cancel ( )
} )
}
private func startSSE ( _ connection : NWConnection ) {
let headers = " " "
HTTP / 1.1 200 OK \ r \ n \
Content - Type : text / event - stream \ r \ n \
Cache - Control : no - cache \ r \ n \
Connection : keep - alive \ r \ n \
\ r \ n
" " "
connection . send ( content : headers . data ( using : . utf8 ) , completion : . contentProcessed { [ weak self ] error in
if error != nil {
connection . cancel ( )
return
}
self ? . addClient ( connection )
let initial = self ? . sseEvent ( name : " feed " , payload : self ? . initialFeedJSON ( ) ? ? " {} " ) ? ? Data ( )
connection . send ( content : initial , completion : . contentProcessed { _ in } )
let status = self ? . sseEvent ( name : " status " , payload : self ? . statusJSON ( ) ? ? " {} " ) ? ? Data ( )
connection . send ( content : status , completion : . contentProcessed { _ in } )
} )
}
private func addClient ( _ connection : NWConnection ) {
let key = ObjectIdentifier ( connection )
clients [ key ] = SSEClient ( connection : connection )
DispatchQueue . main . async {
self . sseClientCount = self . clients . count
}
}
private func removeClient ( for connection : NWConnection ) {
let key = ObjectIdentifier ( connection )
if clients . removeValue ( forKey : key ) != nil {
DispatchQueue . main . async {
self . sseClientCount = self . clients . count
}
}
}
private func closeAllClients ( ) {
for client in clients . values {
client . connection . cancel ( )
}
clients . removeAll ( )
DispatchQueue . main . async {
self . sseClientCount = 0
}
}
private func startHeartbeat ( ) {
let timer = DispatchSource . makeTimerSource ( queue : queue )
timer . schedule ( deadline : . now ( ) + 15 , repeating : 15 )
timer . setEventHandler { [ weak self ] in
guard let self = self else { return }
let data = self . sseEvent ( name : " ping " , payload : " {} " )
self . broadcast ( data : data )
}
timer . resume ( )
heartbeatTimer = timer
}
private func stopHeartbeat ( ) {
heartbeatTimer ? . cancel ( )
heartbeatTimer = nil
}
private func startAddressUpdates ( ) {
updateLocalAddresses ( )
let timer = DispatchSource . makeTimerSource ( queue : queue )
timer . schedule ( deadline : . now ( ) + 10 , repeating : 10 )
timer . setEventHandler { [ weak self ] in
self ? . updateLocalAddresses ( )
}
timer . resume ( )
addressTimer = timer
}
private func stopAddressUpdates ( ) {
addressTimer ? . cancel ( )
addressTimer = nil
}
private func updateLocalAddresses ( ) {
let addresses = Self . localInterfaceAddresses ( )
DispatchQueue . main . async {
self . localAddresses = addresses
}
}
private func startLocalNetworkPrompt ( ) {
guard browser = = nil else { return }
let parameters = NWParameters . tcp
let browser = NWBrowser ( for : . bonjour ( type : " _http._tcp " , domain : nil ) , using : parameters )
browser . stateUpdateHandler = { _ in }
browser . browseResultsChangedHandler = { _ , _ in }
self . browser = browser
browser . start ( queue : queue )
}
private func stopLocalNetworkPrompt ( ) {
browser ? . cancel ( )
browser = nil
}
private func broadcast ( data : Data ) {
for ( key , client ) in clients {
client . connection . send ( content : data , completion : . contentProcessed { [ weak self ] error in
if error != nil {
self ? . clients . removeValue ( forKey : key )
DispatchQueue . main . async {
self ? . sseClientCount = self ? . clients . count ? ? 0
}
}
} )
}
}
2026-01-10 00:25:36 +00:00
private func jsonLine ( from envelope : FeedEnvelope ) -> String {
2026-01-08 19:16:32 +00:00
let encoder = JSONEncoder ( )
if let data = try ? encoder . encode ( envelope ) ,
let string = String ( data : data , encoding : . utf8 ) {
return string
}
return " {} "
}
private func initialFeedJSON ( ) -> String {
return " { \" schema \" :1, \" generated_at \" :1767716400, \" feed \" :[{ \" id \" : \" demo:welcome \" , \" type \" : \" INFO \" , \" title \" : \" Glass Now online \" , \" subtitle \" : \" Connected to iPhone \" , \" priority \" :0.8, \" ttl_sec \" :86400, \" bucket \" : \" RIGHT_NOW \" , \" actions \" :[ \" DISMISS \" ]},{ \" id \" : \" demo:next \" , \" type \" : \" INFO \" , \" title \" : \" Next: Calendar \" , \" subtitle \" : \" Then Weather + POI \" , \" priority \" :0.4, \" ttl_sec \" :86400, \" bucket \" : \" FYI \" , \" actions \" :[ \" DISMISS \" ]}], \" meta \" :{ \" winner_id \" : \" demo:welcome \" , \" unread_count \" :2}} "
}
private func statusJSON ( ) -> String {
let uptime = Int ( Date ( ) . timeIntervalSince ( startDate ) )
return " { \" server \" : \" iphone \" , \" version \" : \" v1 \" , \" uptime_sec \" : \( uptime ) } "
}
private func sseEvent ( name : String , payload : String ? ) -> Data {
let dataLine = payload ? ? " {} "
let message = " event: \( name ) \n " + " data: \( dataLine ) \n \n "
return Data ( message . utf8 )
}
}
private struct SSEClient {
let connection : NWConnection
}
private extension LocalServer {
static func localInterfaceAddresses ( ) -> [ String ] {
var results : [ String ] = [ ]
var addrList : UnsafeMutablePointer < ifaddrs > ?
guard getifaddrs ( & addrList ) = = 0 , let firstAddr = addrList else {
return results
}
defer { freeifaddrs ( addrList ) }
var ptr : UnsafeMutablePointer < ifaddrs > ? = firstAddr
while let addr = ptr ? . pointee {
let flags = Int32 ( addr . ifa_flags )
let isUp = ( flags & IFF_UP ) != 0
let isLoopback = ( flags & IFF_LOOPBACK ) != 0
guard isUp , ! isLoopback , let sa = addr . ifa_addr else {
ptr = addr . ifa_next
continue
}
let family = sa . pointee . sa_family
if family = = UInt8 ( AF_INET ) {
var hostname = [ CChar ] ( repeating : 0 , count : Int ( NI_MAXHOST ) )
let result = getnameinfo (
sa ,
socklen_t ( sa . pointee . sa_len ) ,
& hostname ,
socklen_t ( hostname . count ) ,
nil ,
0 ,
NI_NUMERICHOST
)
if result = = 0 , let ip = String ( validatingUTF8 : hostname ) {
let name = String ( cString : addr . ifa_name )
results . append ( " \( name ) : \( ip ) " )
}
}
ptr = addr . ifa_next
}
return results . sorted ( )
}
}