// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Where a request goes: scheme, host and port, the unit connections are kept by (RFC 6454).
priv struct Origin {
scheme : String
host : String
port : Int32
}
///|
fn Origin::of(url : @moonurl.Url) -> Origin {
let scheme = url.scheme.unwrap_or("http")
let port = match url.port {
Some(p) => p
None => if scheme == "https" { 443 } else { 80 }
}
{ scheme, host: url.host.unwrap_or(""), port, }
}
///|
fn Origin::key(self : Origin) -> String {
"\{self.scheme}://\{self.host}:\{self.port}"
}
///|
/// The host and port as a request names them: the port left out when it is the scheme's own.
fn Origin::authority(self : Origin) -> String {
let host = if self.host.contains(":") { "[\{self.host}]" } else { self.host }
match (self.scheme, self.port) {
("http", 80) | ("https", 443) => host
_ => "\{host}:\{self.port}"
}
}
///|
/// A connection to an origin, in whichever version ALPN settled on.
priv enum Link {
H1(@tcp.Conn, reused~ : Bool)
H2(H2, reused~ : Bool)
}
///|
/// Idle HTTP/1.1 connections, a few per origin, and one HTTP/2 connection per origin, which
/// carries every request to it. Both maps are keyed by the origin.
priv struct Pool {
idle : Map[String, Array[@tcp.Conn]]
h2 : Map[String, H2]
keep : Int32
/// One dial at a time: without it a hundred requests that start together open a hundred
/// connections and throw all but one away.
dial : @async.Mutex
}
///|
fn Pool::new(keep : Int32) -> Pool {
{ idle: Map([]), h2: Map([]), keep, dial: @async.Mutex(), }
}
///|
/// The HTTP/2 connection for an origin, when it can still take work.
fn Pool::live(self : Pool, key : String) -> H2? {
match self.h2.get(key) {
Some(h2) if !h2.dead && !h2.going => Some(h2)
Some(h2) => {
self.forget(key, h2)
None
}
None => None
}
}
///|
fn Pool::keep_h2(self : Pool, key : String, h2 : H2) -> Unit {
self.h2[key] = h2
}
///|
/// Take an idle connection for the origin, or `None`.
fn Pool::take(self : Pool, key : String) -> @tcp.Conn? {
match self.idle.get(key) {
Some(conns) => conns.pop()
None => None
}
}
///|
/// Drop an HTTP/2 connection that has failed or been told to go away.
fn Pool::forget(self : Pool, key : String, h2 : H2) -> Unit {
h2.dead = true
if self.h2.get(key) is Some(kept) && physical_equal(kept, h2) {
self.h2.remove(key)
}
h2.conn.close()
}
///|
/// Keep a connection for the next request to its origin, or close it when enough are kept.
fn Pool::checkin(self : Pool, key : String, conn : @tcp.Conn) -> Unit {
let conns = match self.idle.get(key) {
Some(conns) => conns
None => {
let conns = []
self.idle[key] = conns
conns
}
}
if conns.length() < self.keep {
conns.push(conn)
} else {
conn.close()
}
}
///|
fn Pool::close(self : Pool) -> Unit {
for _, conns in self.idle {
for conn in conns {
conn.close()
}
}
for _, h2 in self.h2 {
h2.conn.close()
}
self.idle.clear()
self.h2.clear()
}
///|
/// How much is read from a connection at once: a TLS record's worth. The async default is a
/// kibibyte.
let chunk : Int32 = 16384