char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
38d7f6c
main
1import { 2NumberIsFinite , 3ObjectCreate , 4ObjectFreeze , 5RangeError , 6ReflectApply as apply , 7Set , 8SetPrototypeAdd as add , 9SetPrototypeHas as has , 10TypedArrayPrototypeGetTag as tag , 11TypeError , 12WeakMap , 13WeakMapPrototypeGet as get , 14WeakMapPrototypeSet as set , 15} from "ext:core/primordials" ; 16import { reportNonfatalException } from "ext:core/event_hooks" ; 17import { 18markPromiseHandled , 19promiseThen as then , 20safePromise , 21} from "ext:core/promises" ; 22import { 23op_http_cancel , 24op_http_cli_listen , 25op_http_finished , 26op_http_listen , 27op_http_next , 28op_http_read , 29op_http_respond , 30op_http_respond_file , 31op_http_stop , 32op_http_upgrade , 33op_http_wait , 34op_http_write , 35} from "ext:core/ops" ; 36import { dictionary , state , usv } from "ext:web/idl.js" ; 37import { 38abort , 39AbortSignal , 40addListener , 41DOMException , 42removeListener , 43secret , 44signals , 45} from "ext:web/events.js" ; 46import { 47enqueue , 48errorStream , 49finish , 50read , 51ReadableStream , 52readerCancel , 53release , 54streamCancel , 55streamReader , 56} from "ext:webstreams/streams.js" ; 57import { bodies , messageState , usable } from "ext:http_types/body.js" ; 58import { fromPairs , headerEntries , headers } from "ext:http_types/headers.js" ; 59import { Request } from "ext:http_types/request.js" ; 60import { Response } from "ext:http_types/response.js" ; 61import { adoptWebSocket } from "ext:websocket/websocket.js" ; 62import { takeFile } from "./file.js" ; 63 64import type { 65HttpAddress , 66HttpListener , 67HttpRequestMetadata , 68} from "ext:core/ops" ; 69import type { 70ReadableByteStreamController , 71ReadableStreamDefaultReader , 72} from "ext:webstreams/streams.js" ; 73import type { WebSocket } from "ext:websocket/websocket.js" ; 74 75import type { ServeOptions , UnixServeOptions } from "avril:std" ; 76 77type StreamAddr = Readonly < HttpAddress >; 78type TcpAddr = Extract < StreamAddr , { transport :"tcp" }>; 79type UnixAddr = Extract < StreamAddr , { transport :"unix" }>; 80interface ServeInfo < A extends StreamAddr = StreamAddr > { 81readonly remoteAddr :A ; 82} 83type ServeHandler < A extends StreamAddr = StreamAddr > = ( request :Request , info :ServeInfo < A >) => unknown ; 84type ErrorHandler = ( error :unknown ) => unknown ; 85interface HttpServer < A extends StreamAddr = StreamAddr > { 86readonly addr :A ; 87readonly finished :Promise < void >; 88readonly shutdown :() => Promise < void >; 89} 90 91const requests = new WeakMap < object , number >(); 92 93function address ( value :HttpAddress ) :StreamAddr { 94return ObjectFreeze ( 95value . transport === "unix" 96 ?{ __proto__ :null , transport :"unix" , path :value . path } 97 :{ 98__proto__ :null , 99transport :"tcp" , 100hostname :value . hostname , 101port :value . port , 102}, 103); 104} 105 106function upgradeWebSocket ( 107request :unknown , 108rawOptions :unknown = {}, 109) :{ socket :WebSocket ; response :Response } { 110const id = get ( requests , request ); 111if ( id === undefined ) { 112throw new TypeError ( "Expected an incoming HTTP request" ); 113} 114const requestState = messageState ( request , "request" ); 115usable ( requestState . body ); 116const options = dictionary ( rawOptions ); 117const protocol = options . protocol === undefined 118 ?null 119 :usv ( options . protocol ); 120const bound = op_http_upgrade ([ id , protocol ]); 121const socket = adoptWebSocket ( bound [ 0 ], requestState . url ); 122const response = ObjectCreate ( Response . prototype ) as Response , 123h = fromPairs ( bound [ 1 ]); 124state ( headers , h ). immutable = true ; 125set ( bodies , response , { 126kind :"response" , 127body :{ stream :null , bytes :null , used :false , type :null }, 128headers :h , 129status :101 , 130statusText :"Switching Protocols" , 131type :"default" , 132url :"" , 133redirected :false , 134upgrade :id , 135}); 136return { socket, response}; 137} 138 139async function dispatch ( 140metadata :HttpRequestMetadata , 141handler :ServeHandler , 142onError :ErrorHandler | undefined , 143) :Promise < void > { 144const id = metadata [ 0 ], signal = new AbortSignal ( secret ); 145let controller :ReadableByteStreamController | undefined , 146reader :ReadableStreamDefaultReader < unknown > | undefined , 147bodyEnded = ! metadata [ 4 ], 148sent = false ; 149const reported = new Set < unknown >(); 150function report ( error :unknown ) :void { 151if ( state ( signals , signal ). aborted || has ( reported , error )) return ; 152add ( reported , error ); 153reportNonfatalException ( error ); 154} 155const disconnected = new DOMException ( 156"HTTP client disconnected" , 157"AbortError" , 158); 159then ( op_http_wait ( id ), ( canceled ) => { 160if ( canceled ) abort ( signal , disconnected ); 161if ( ! bodyEnded && controller ) errorStream ( controller , disconnected ); 162if ( reader ) markPromiseHandled ( readerCancel ( reader , disconnected )); 163}, () => abort ( signal , disconnected )); 164const stream = metadata [ 4 ] 165 ?new ReadableStream < Uint8Array < ArrayBuffer >>({ 166__proto__ :null , 167type :"bytes" , 168start ( c ) { 169controller = c ; 170}, 171async pull ( c ) { 172try { 173const bytes = await safePromise ( op_http_read ( id )); 174if ( bytes === null ) { 175bodyEnded = true ; 176finish ( c ); 177} else if ( bytes . length ) enqueue ( c , bytes ); 178} catch ( error ) { 179bodyEnded = true ; 180errorStream ( c , error ); 181} 182}, 183cancel () { 184bodyEnded = true ; 185}, 186}, { __proto__ :null , highWaterMark :0 }) 187 :null ; 188const h = fromPairs ( metadata [ 3 ]); 189state ( headers , h ). immutable = true ; 190const request = ObjectCreate ( Request . prototype ) as Request ; 191set ( bodies , request , { 192kind :"request" , 193url :metadata [ 2 ], 194method :metadata [ 1 ], 195headers :h , 196 signal, 197redirect :"follow" , 198cache :"default" , 199credentials :"same-origin" , 200mode :"cors" , 201integrity :"" , 202keepalive :false , 203referrer :"" , 204referrerPolicy :"" , 205priority :"auto" , 206body :{ stream, bytes :null , used :false , type :null }, 207}); 208set ( requests , request , id ); 209const info = ObjectFreeze ({ 210__proto__ :null , 211remoteAddr :address ( metadata [ 5 ]), 212}); 213try { 214let response :unknown ; 215try { 216response = await safePromise ( handler ( request , info )); 217const s = messageState ( response , "response" ); 218usable ( s . body ); 219if ( s . status < 200 && ! ( s . status === 101 && s . upgrade === id )) { 220throw new TypeError ( "Invalid HTTP response status" ); 221} 222} catch ( error ) { 223if ( ! onError ) report ( error ); 224try { 225response = onError 226 ?await safePromise ( onError ( error )) 227 :new Response ( "Internal Server Error" , { status :500 }); 228const s = messageState ( response , "response" ); 229usable ( s . body ); 230if ( s . status < 200 && ! ( s . status === 101 && s . upgrade === id )) { 231throw new TypeError ( "Invalid HTTP response status" ); 232} 233} catch ( error ) { 234report ( error ); 235response = new Response ( "Internal Server Error" , { status :500 }); 236} 237} 238const s = messageState ( response , "response" ), b = s . body ; 239if ( state ( signals , signal ). aborted ) { 240if ( b . stream ) markPromiseHandled ( streamCancel ( b . stream , disconnected )); 241return ; 242} 243const file = b . stream === null ?null :takeFile ( b . stream , signal ); 244if ( file !== null ) { 245b . used = true ; 246op_http_respond_file ([ id , s . status , headerEntries ( s . headers ), file ]); 247sent = true ; 248return ; 249} 250const streaming = b . stream !== null && b . bytes === null ; 251if ( streaming ) reader = streamReader ( b . stream ); 252b . used = b . stream !== null ; 253const wantsBody = op_http_respond ([ 254id , 255s . status , 256headerEntries ( s . headers ), 257b . bytes , 258streaming , 259]); 260sent = true ; 261if ( b . bytes !== null ) markPromiseHandled ( streamCancel ( b . stream )); 262if ( reader ) { 263if ( wantsBody ) { 264while ( true ) { 265const result = await safePromise ( read ( reader )); 266if ( result . done ) break ; 267if ( tag ( result . value ) !== "Uint8Array" ) { 268throw new TypeError ( "Response chunks must be Uint8Array" ); 269} 270// The native write still checks shared and resizable backing stores. 271if ( 272! await safePromise ( 273op_http_write ([ id , result . value as Uint8Array < ArrayBuffer >]), 274) 275) { 276markPromiseHandled ( readerCancel ( reader , disconnected )); 277return ; 278} 279} 280await safePromise ( op_http_write ([ id , null ])); 281} else markPromiseHandled ( readerCancel ( reader )); 282} 283} catch ( error ) { 284report ( error ); 285if ( ! sent ) { 286try { 287const fallback = new Response ( "Internal Server Error" , { status :500 }); 288const s = messageState ( fallback , "response" ); 289op_http_respond ([ 290id , 291s . status , 292headerEntries ( s . headers ), 293s . body . bytes , 294false , 295]); 296} catch { 297op_http_cancel ( id ); 298} 299} else op_http_cancel ( id ); 300if ( reader ) markPromiseHandled ( readerCancel ( reader )); 301} finally { 302if ( reader ) release ( reader ); 303reader = undefined ; 304} 305} 306 307function serve ( handler :ServeHandler < TcpAddr >) :HttpServer < TcpAddr >; 308function serve ( options :ServeOptions , handler :ServeHandler < TcpAddr >) :HttpServer < TcpAddr >; 309function serve ( options :UnixServeOptions , handler :ServeHandler < UnixAddr >) :HttpServer < UnixAddr >; 310function serve ( rawOptions :unknown , handler :unknown = undefined ) :HttpServer { 311if ( typeof rawOptions === "function" ) { 312handler = rawOptions ; 313rawOptions = {}; 314} 315const options = dictionary ( rawOptions ); 316if ( typeof handler !== "function" ) { 317throw new TypeError ( "serve requires a handler" ); 318} 319const hostname = options . hostname === undefined 320 ?"127.0.0.1" 321 :usv ( options . hostname ); 322const port = options . port === undefined ?8000 :+ ( options . port as number ); 323if ( ! NumberIsFinite ( port ) || port < 0 || port > 65535 || port % 1 !== 0 ) { 324throw new RangeError ( "Invalid HTTP listen port" ); 325} 326const signal = options . signal , 327onError = options . onError , 328onListen = options . onListen ; 329if ( onError !== undefined && typeof onError !== "function" ) { 330throw new TypeError ( "Invalid onError callback" ); 331} 332if ( onListen !== undefined && typeof onListen !== "function" ) { 333throw new TypeError ( "Invalid onListen callback" ); 334} 335const path = options . path === undefined ?undefined :usv ( options . path ); 336if ( 337path !== undefined && 338( options . hostname !== undefined || options . port !== undefined ) 339) { 340throw new TypeError ( 341"Unix socket path cannot be combined with hostname or port" , 342); 343} 344if ( signal !== undefined && state ( signals , signal ). aborted ) { 345throw state ( signals , signal ). reason ; 346} 347const bound = op_http_listen ( 348path === undefined 349 ?{ __proto__ :null , transport :"tcp" , hostname, port} 350 :{ __proto__ :null , transport :"unix" , path}, 351); 352return startServer ( 353bound , 354handler as ServeHandler , 355onError as ErrorHandler | undefined , 356signal , 357onListen as (( addr :StreamAddr ) => unknown ) | undefined , 358); 359} 360 361function startServer ( 362bound :HttpListener , 363handler :ServeHandler , 364onError :ErrorHandler | undefined , 365signal :unknown , 366onListen :(( addr :StreamAddr ) => unknown ) | undefined , 367) :HttpServer { 368const id = bound [ 0 ], addr = address ( bound [ 1 ]); 369const stop = () => op_http_stop ([ id , true ]); 370if ( signal !== undefined ) addListener ( signal , "abort" , stop , { once :true }); 371const finished = then ( op_http_finished ( id ), () => { 372if ( signal !== undefined ) removeListener ( signal , "abort" , stop ); 373}, ( error ) => { 374if ( signal !== undefined ) removeListener ( signal , "abort" , stop ); 375throw error ; 376}); 377markPromiseHandled ( finished ); 378then ( 379( async () => { 380while ( true ) { 381const metadata = await safePromise ( op_http_next ( id )); 382if ( metadata === null ) break ; 383markPromiseHandled ( dispatch ( metadata , handler , onError )); 384} 385})(), 386() => {}, 387stop , 388); 389const server = ObjectFreeze ({ 390__proto__ :null , 391 addr, 392 finished, 393shutdown () { 394op_http_stop ([ id , false ]); 395return finished ; 396}, 397}); 398try { 399if ( onListen ) onListen ( addr ); 400} catch ( error ) { 401stop (); 402throw error ; 403} 404return server ; 405} 406 407function startCLI ( namespace :Record < string , unknown >) :Promise < void > { 408const module = namespace . default ; 409if ( module === null || typeof module !== "object" ) { 410throw new TypeError ( 411"avril serve requires a default export object with a fetch function" , 412); 413} 414const fetch = ( module as Record < string , unknown >). fetch ; 415if ( typeof fetch !== "function" ) { 416throw new TypeError ( 417"avril serve requires a default export object with a fetch function" , 418); 419} 420const bound = op_http_cli_listen ( null ); 421const server = startServer ( 422bound , 423( request , info ) => apply ( fetch , module , [ request , info ]), 424undefined , 425undefined , 426undefined , 427); 428return server . finished ; 429} 430 431export type { HttpServer , ServeHandler , ServeInfo , StreamAddr }; 432export { serve , startCLI , upgradeWebSocket };