char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f2c35fd
main
1import { 2ArrayPrototypePush as push , 3getter , 4MathMin as min , 5NumberParseInt , 6ObjectCreate , 7ObjectDefineProperty as define , 8ReflectApply as apply , 9ReflectConstruct as construct , 10RegExpPrototypeExec as exec , 11String , 12StringPrototypeSlice as slice , 13TypedArrayPrototypeGetLength as length , 14uncurryThis , 15WeakMap , 16WeakMapPrototypeSet as set , 17} from "ext:core/primordials" ; 18import { 19markPromiseHandled , 20promiseThen as then , 21safePromise , 22} from "ext:core/promises" ; 23import { enqueueTask } from "ext:core/tasks" ; 24import { dictionary , required , state , usv } from "ext:web/idl.js" ; 25import { userAgent } from "ext:web/navigator.js" ; 26import { 27addListener , 28createException , 29dispatch , 30Event , 31EventTarget , 32preventDefault , 33removeListener , 34} from "ext:web/events.js" ; 35import { registerUncloneable } from "ext:web/serialization.js" ; 36import { MessageEvent } from "ext:web/messaging.js" ; 37import { TextDecoder , TextEncoder } from "ext:encoding/main.js" ; 38import { URL } from "ext:url/main.js" ; 39import { clearTimeout , setTimeout } from "ext:timers/timers.js" ; 40import { 41op_fetch_base , 42op_fetch_cancel , 43op_fetch_create , 44op_fetch_read , 45op_fetch_send , 46} from "ext:core/ops" ; 47import { extractMime } from "ext:http_types/mime.js" ; 48 49const CONNECTING = 0 , 50OPEN = 1 , 51CLOSED = 2 ; 52type ReadyState = typeof CONNECTING | typeof OPEN | typeof CLOSED ; 53type EventType = "open" | "message" | "error" ; 54type EventHandler < E extends Event > = ( this :EventSource , event :E ) => unknown ; 55interface Handler { 56value :Function | null ; 57listener :(( event :Event ) => void ) | null ; 58} 59interface SourceState { 60self :EventSource ; 61url :string ; 62origin :string ; 63withCredentials :boolean ; 64state :ReadyState ; 65lastEventId :string ; 66delay :number ; 67resource :number | undefined ; 68timer :number | undefined ; 69handlers :Record < EventType , Handler >; 70pending :string ; 71skipLF :boolean ; 72data :string ; 73type :string ; 74id :string ; 75} 76 77const sources = new WeakMap < object , SourceState >(); 78registerUncloneable ( sources ); 79const streaming = { __proto__ :null , stream :true }; 80const digits = / ^[0-9]+$ / ; 81const decode = uncurryThis ( TextDecoder . prototype . decode ), 82encode = uncurryThis ( TextEncoder . prototype . encode ), 83urlHref = getter ( URL . prototype , "href" ), 84urlOrigin = getter ( URL . prototype , "origin" ), 85urlProtocol = getter ( URL . prototype , "protocol" ); 86const encoder = new TextEncoder (); 87const fromCode = String . fromCharCode , 88codeAt = uncurryThis ( String . prototype . charCodeAt ), 89indexOf = uncurryThis ( String . prototype . indexOf ); 90 91function syntaxError ( message :string ) :Error { 92return createException ({ message, name :"SyntaxError" }); 93} 94 95// Reconnect requests carry the raw UTF-8 bytes of the last event ID, which 96// op_fetch_send re-encodes as one byte per character. 97function byteHeaderValue ( value :string ) :string | null { 98const bytes = encode ( encoder , value ); 99let result = "" ; 100for ( let i = 0 ; i < length ( bytes ); i ++ ) { 101const byte = bytes [ i ]; 102// Valid SSE IDs can contain controls that hyper cannot send in headers. 103if (( byte < 32 && byte !== 9 ) || byte === 127 ) return null ; 104result += fromCode ( byte ); 105} 106return result ; 107} 108 109function contentType ( list :readonly [ name :string , value :string ][]) :string { 110let value :string | null = null ; 111for ( let i = 0 ; i < list . length ; i ++ ) { 112if ( list [ i ][ 0 ] === "content-type" ) { 113value = value === null ?list [ i ][ 1 ] :value + "," + list [ i ][ 1 ]; 114} 115} 116return extractMime ( value )?. essence ?? "" ; 117} 118 119const finish = () => true ; 120 121function failure ( s :SourceState ) :void { 122enqueueTask (() => { 123if ( s . state === CLOSED ) return ; 124s . state = CONNECTING ; 125dispatch ( s . self , new Event ( "error" )); 126// The handler can close the source synchronously. 127if (( s . state as ReadyState ) === CLOSED ) return ; 128s . timer = setTimeout (() => { 129s . timer = undefined ; 130if ( s . state === CONNECTING ) connect ( s ); 131}, s . delay ); 132}, finish ); 133} 134 135function queueEvent ( 136s :SourceState , 137type :string , 138data :string , 139id :string , 140) :void { 141const origin = s . origin ; 142enqueueTask (() => { 143if ( s . state === CLOSED ) return ; 144dispatch ( 145s . self , 146new MessageEvent ( type , { 147__proto__ :null , 148 data, 149 origin, 150lastEventId :id , 151}), 152); 153}, finish ); 154} 155 156function feed ( s :SourceState , text :string ) :void { 157if ( text === "" ) return ; 158let start = 0 ; 159for ( let i = 0 ; i < text . length ; i ++ ) { 160const code = codeAt ( text , i ); 161if ( s . skipLF ) { 162s . skipLF = false ; 163// The lone LF after a chunk-split CR belongs to the same line break. 164if ( code === 10 ) { 165start = i + 1 ; 166continue ; 167} 168} 169if ( code === 13 || code === 10 ) { 170line ( s , s . pending + slice ( text , start , i )); 171s . pending = "" ; 172start = i + 1 ; 173s . skipLF = code === 13 ; 174} 175} 176s . pending += slice ( text , start ); 177} 178 179function line ( s :SourceState , value :string ) :void { 180if ( value === "" ) { 181s . lastEventId = s . id ; 182if ( s . data === "" ) { 183s . type = "" ; 184return ; 185} 186const data = slice ( s . data , 0 , s . data . length - 1 ), 187type = s . type === "" ?"message" :s . type , 188id = s . lastEventId ; 189s . data = "" ; 190s . type = "" ; 191queueEvent ( s , type , data , id ); 192return ; 193} 194const colon = indexOf ( value , ":" ); 195let field , body ; 196if ( colon === - 1 ) { 197field = value ; 198body = "" ; 199} else { 200field = slice ( value , 0 , colon ); 201body = slice ( value , colon + 1 ); 202if ( codeAt ( body , 0 ) === 32 ) body = slice ( body , 1 ); 203} 204if ( field === "data" ) s . data = s . data + body + "\n" ; 205else if ( field === "event" ) s . type = body ; 206else if ( field === "id" ) { 207if ( indexOf ( body , "\u0000" ) === - 1 ) s . id = body ; 208} else if ( field === "retry" && exec ( digits , body )) { 209s . delay = min ( NumberParseInt ( body , 10 ), 2147483647 ); 210} 211} 212 213async function consume ( 214s :SourceState , 215resource :number , 216decoder :TextDecoder , 217) :Promise < void > { 218try { 219while ( s . state !== CLOSED ) { 220const bytes = await safePromise ( op_fetch_read ( resource )); 221// Closing can race the pending read. 222if ( bytes === null || ( s . state as ReadyState ) === CLOSED ) break ; 223feed ( s , decode ( decoder , bytes , streaming )); 224} 225} catch { 226// Interrupted streams follow the same reconnection path as EOF. 227} finally { 228decode ( decoder ); 229op_fetch_cancel ( resource ); 230s . resource = undefined ; 231} 232if ( s . state !== CLOSED ) failure ( s ); 233} 234 235function connect ( s :SourceState ) :void { 236const resource = op_fetch_create ( null ); 237s . resource = resource ; 238// Preserve the resume cursor until the server replaces or clears it. 239s . pending = "" ; 240s . skipLF = false ; 241s . data = "" ; 242s . type = "" ; 243s . id = s . lastEventId ; 244const entries :[ name :string , value :string ][] = [ 245[ "accept" , "text/event-stream" ], 246[ "cache-control" , "no-cache" ], 247[ "pragma" , "no-cache" ], 248[ "user-agent" , userAgent ], 249]; 250if ( s . lastEventId !== "" ) { 251const value = byteHeaderValue ( s . lastEventId ); 252if ( value !== null ) push ( entries , [ "last-event-id" , value ]); 253} 254const pending = op_fetch_send ([ 255resource , 256s . url , 257"GET" , 258entries , 259null , 260false , 261"follow" , 262]); 263then ( 264pending , 265( reply ) => { 266if ( s . state === CLOSED ) return ; 267const status = reply [ 0 ]; 268if ( status !== 200 || contentType ( reply [ 2 ]) !== "text/event-stream" ) { 269op_fetch_cancel ( resource ); 270s . resource = undefined ; 271enqueueTask (() => { 272if ( s . state !== CONNECTING ) return ; 273s . state = CLOSED ; 274dispatch ( s . self , new Event ( "error" )); 275}, finish ); 276return ; 277} 278s . origin = urlOrigin ( new URL ( reply [ 3 ])); 279enqueueTask (() => { 280if ( s . state !== CONNECTING ) { 281op_fetch_cancel ( resource ); 282return ; 283} 284s . state = OPEN ; 285dispatch ( s . self , new Event ( "open" )); 286}, finish ); 287markPromiseHandled ( consume ( s , resource , new TextDecoder ())); 288}, 289() => { 290op_fetch_cancel ( resource ); 291s . resource = undefined ; 292if ( s . state === CLOSED ) return ; 293failure ( s ); 294}, 295); 296} 297 298const eventTypes = [ "open" , "message" , "error" ] as const ; 299 300function handler ( type :EventType ) { 301return { 302get ( this :EventSource ) :Function | null { 303return state ( sources , this ). handlers [ type ]. value ; 304}, 305set ( this :EventSource , rawValue :unknown ) :void { 306const handler = state ( sources , this ). handlers [ type ]; 307const value = typeof rawValue === "function" ?rawValue :null ; 308handler . value = value ; 309if ( value && ! handler . listener ) { 310handler . listener = ( event ) => { 311if ( apply ( handler . value ! , this , [ event ]) === false ) { 312preventDefault ( event ); 313} 314}; 315addListener ( this , type , handler . listener ); 316} else if ( ! value && handler . listener ) { 317removeListener ( this , type , handler . listener ); 318handler . listener = null ; 319} 320}, 321configurable :true , 322}; 323} 324 325interface EventSource { 326readonly CONNECTING :typeof CONNECTING ; 327readonly OPEN :typeof OPEN ; 328readonly CLOSED :typeof CLOSED ; 329onopen :EventHandler < Event > | null ; 330onmessage :EventHandler < MessageEvent > | null ; 331onerror :EventHandler < Event > | null ; 332} 333 334class EventSource extends EventTarget { 335declare static readonly CONNECTING :typeof CONNECTING ; 336declare static readonly OPEN :typeof OPEN ; 337declare static readonly CLOSED :typeof CLOSED ; 338 339// @ts-expect-error Reflect.construct returns the instance without a guest-replaceable super() call. 340constructor ( rawUrl :unknown , init :unknown = {}) { 341required ( arguments . length , 1 ); 342const url = usv ( rawUrl ); 343const options = dictionary ( init ); 344const withCredentials = !! options . withCredentials ; 345let record ; 346try { 347record = new URL ( url , op_fetch_base () || undefined ); 348} catch { 349throw syntaxError ( "The URL is invalid" ); 350} 351const self = construct ( EventTarget , [], new . target ) as EventSource ; 352const handlers = ObjectCreate ( null ) as Record < EventType , Handler >; 353for ( let i = 0 ; i < eventTypes . length ; i ++ ) { 354handlers [ eventTypes [ i ]] = { value :null , listener :null }; 355} 356const s :SourceState = { 357 self, 358url :urlHref ( record ), 359origin :urlOrigin ( record ), 360 withCredentials, 361state :CONNECTING , 362lastEventId :"" , 363delay :3000 , 364resource :undefined , 365timer :undefined , 366 handlers, 367pending :"" , 368skipLF :false , 369data :"" , 370type :"" , 371id :"" , 372}; 373set ( sources , self , s ); 374const protocol = urlProtocol ( record ); 375if ( protocol === "http:" || protocol === "https:" ) connect ( s ); 376else { 377enqueueTask (() => { 378if ( s . state === CLOSED ) return ; 379s . state = CLOSED ; 380dispatch ( self , new Event ( "error" )); 381}, finish ); 382} 383return self ; 384} 385get url () { 386return state ( sources , this ). url ; 387} 388get withCredentials () { 389return state ( sources , this ). withCredentials ; 390} 391get readyState () { 392return state ( sources , this ). state ; 393} 394close () :void { 395const s = state ( sources , this ); 396if ( s . state === CLOSED ) return ; 397s . state = CLOSED ; 398if ( s . resource !== undefined ) op_fetch_cancel ( s . resource ); 399s . resource = undefined ; 400if ( s . timer !== undefined ) { 401clearTimeout ( s . timer ); 402s . timer = undefined ; 403} 404} 405} 406 407for ( 408const [ name , value ] of [ 409[ "CONNECTING" , 0 ], 410[ "OPEN" , 1 ], 411[ "CLOSED" , 2 ], 412] as const 413) { 414define ( EventSource , name , { value, enumerable :true }); 415define ( EventSource . prototype , name , { value, enumerable :true }); 416} 417for ( const type of eventTypes ) { 418define ( EventSource . prototype , `on ${ type } ` , handler ( type )); 419} 420 421export { EventSource };