char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
ec3de1c
main
1import { 2ArrayBuffer , 3ArrayBufferPrototypeGetResizable as resizable , 4ArrayPrototypePush as push , 5getter , 6ObjectCreate , 7ObjectDefineProperty as define , 8ReflectApply as apply , 9ReflectConstruct as construct , 10RegExp , 11RegExpPrototypeExec as exec , 12Set , 13SetPrototypeAdd as add , 14SetPrototypeHas as has , 15StringPrototypeSlice as slice , 16SymbolIterator , 17TypedArrayPrototypeGetBuffer as buffer , 18TypedArrayPrototypeGetLength as length , 19TypedArrayPrototypeGetTag as tag , 20TypeError , 21uncurryThis , 22WeakMap , 23WeakMapPrototypeSet as set , 24} from "ext:core/primordials" ; 25import { 26markPromiseHandled , 27promiseThen as then , 28safePromise , 29} from "ext:core/promises" ; 30import { 31dictionary , 32isObject , 33required , 34state , 35string , 36usv , 37} from "ext:web/idl.js" ; 38import { blobBody , fromBytes , snapshot } from "ext:blob/blob.js" ; 39import { MessageEvent } from "ext:web/messaging.js" ; 40import { userAgent } from "ext:web/navigator.js" ; 41import { 42addListener , 43createException , 44dispatch , 45ErrorEvent , 46Event , 47EventTarget , 48preventDefault , 49removeListener , 50reportErrorValue , 51} from "ext:web/events.js" ; 52import { TextEncoder } from "ext:encoding/main.js" ; 53import { URL } from "ext:url/main.js" ; 54import { 55op_ws_close , 56op_ws_create , 57op_ws_fail , 58op_ws_retire , 59op_ws_send , 60op_ws_wait , 61} from "ext:core/ops" ; 62 63import type { WebSocketEvent } from "ext:core/ops" ; 64 65import { byteLength as sharedLength } from "ext:core/shared_array_buffer" ; 66 67const CONNECTING = 0 , 68OPEN = 1 , 69CLOSING = 2 , 70CLOSED = 3 ; 71type ReadyState = 72| typeof CONNECTING 73| typeof OPEN 74| typeof CLOSING 75| typeof CLOSED ; 76type EventType = "open" | "message" | "error" | "close" ; 77type EventHandler < E extends Event > = ( this :WebSocket , event :E ) => unknown ; 78interface Handler { 79value :Function | null ; 80listener :(( event :Event ) => void ) | null ; 81} 82interface SocketState { 83id :number ; 84href :string ; 85origin :string ; 86state :ReadyState ; 87protocol :string ; 88extensions :string ; 89binaryType :"blob" | "arraybuffer" ; 90buffered :number ; 91handlers :Record < EventType , Handler >; 92} 93interface CloseState { 94wasClean :boolean ; 95code :number ; 96reason :string ; 97} 98const sockets = new WeakMap < object , SocketState >(), 99closeEvents = new WeakMap < object , CloseState >(); 100const encoder = new TextEncoder (), 101encode = uncurryThis ( TextEncoder . prototype . encode ); 102const invalidToken = RegExp ( "[^A-Za-z0-9!#$%&'*+\\-.^_`|~]" ); 103const floor = Math . floor , 104includes = uncurryThis ( String . prototype . includes ), 105urlHref = getter ( URL . prototype , "href" ), 106urlProtocol = getter ( URL . prototype , "protocol" ), 107urlOrigin = getter ( URL . prototype , "origin" ); 108// As in blob.js: the byteLength getter throws for anything but an ArrayBuffer. 109const isView = ArrayBuffer . isView , 110byteLength = getter ( ArrayBuffer . prototype , "byteLength" ), 111dataViewBuffer = getter ( DataView . prototype , "buffer" ); 112 113function isBufferSource ( value :unknown ) :boolean { 114if ( isView ( value )) return true ; 115try { 116byteLength ( value ); 117return true ; 118} catch {} 119try { 120sharedLength ( value ); 121return true ; 122} catch { 123return false ; 124} 125} 126 127function syntaxError ( message :string ) :Error { 128return createException ({ message, name :"SyntaxError" }); 129} 130 131function toUint16 ( value :unknown ) :number { 132return ( + ( value as number ) >>> 0 ) % 65536 ; 133} 134 135function clamp ( value :unknown ) :number { 136const number = + ( value as number ); 137if ( number !== number || number <= 0 ) return 0 ; 138if ( number >= 65535 ) return 65535 ; 139const base = floor ( number ), 140fraction = number - base ; 141return fraction < 0.5 || ( fraction === 0.5 && base % 2 === 0 ) 142 ?base 143 :base + 1 ; 144} 145 146function convertProtocols ( value :unknown ) :string [] { 147const list :string [] = []; 148if ( value === undefined ) return list ; 149const method = isObject ( value ) ?value [ SymbolIterator ] :undefined ; 150if ( method === undefined || method === null ) { 151push ( list , string ( value )); 152} else { 153if ( typeof method !== "function" ) { 154throw new TypeError ( "Invalid protocol iterator" ); 155} 156const iterator = apply ( method , value , []); 157if ( ! isObject ( iterator )) throw new TypeError ( "Invalid protocol iterator" ); 158const next = iterator . next ; 159if ( typeof next !== "function" ) { 160throw new TypeError ( "Invalid protocol iterator" ); 161} 162while ( true ) { 163const step = apply ( next , iterator , []); 164if ( ! isObject ( step )) { 165throw new TypeError ( "Invalid protocol iterator result" ); 166} 167if ( step . done ) break ; 168push ( list , string ( step . value )); 169} 170} 171return list ; 172} 173 174// The pump holds the socket strongly until the close event, which is what keeps 175// an OPEN socket with listeners from being collected. 176async function pump ( id :number , socket :WebSocket ) :Promise < void > { 177let failure :string | undefined ; 178try { 179while ( true ) { 180let event :WebSocketEvent ; 181try { 182event = await safePromise ( op_ws_wait ( id )); 183} catch ( error ) { 184failure = usv ( error ); 185event = { kind :"close" , clean :false , code :1006 , reason :"" }; 186} 187const s = state ( sockets , socket ); 188try { 189if ( event . kind === "open" ) { 190// close() may have been called while the connection was still establishing. 191if ( s . state === CONNECTING ) { 192s . state = OPEN ; 193if ( event . protocol !== "" ) s . protocol = event . protocol ; 194dispatch ( socket , new Event ( "open" )); 195} 196} else if ( event . kind === "text" || event . kind === "binary" ) { 197if ( s . state === OPEN ) { 198const data = event . kind === "text" 199 ?event . data 200 :s . binaryType === "blob" 201 ?fromBytes ( event . data , "" ) 202 :buffer ( event . data ); 203dispatch ( 204socket , 205new MessageEvent ( "message" , { data, origin :s . origin }), 206); 207} 208} else if ( event . kind === "closing" ) { 209if ( s . state === OPEN ) s . state = CLOSING ; 210} else if ( event . kind === "error" ) { 211failure = event . message ; 212} else { 213s . state = CLOSED ; 214if ( failure !== undefined ) { 215dispatch ( socket , new ErrorEvent ( "error" , { message :failure })); 216} 217dispatch ( 218socket , 219new CloseEvent ( "close" , { 220wasClean :event . clean , 221code :event . code , 222reason :event . reason , 223}), 224); 225return ; 226} 227} catch ( error ) { 228reportErrorValue ( error ); 229if ( s . state === CLOSED ) return ; 230} 231} 232} finally { 233op_ws_retire ( id ); 234} 235} 236 237const eventTypes :EventType [] = [ "open" , "message" , "error" , "close" ]; 238 239function initializeSocket ( 240socket :WebSocket , 241id :number , 242record :URL , 243) :WebSocket { 244const handlers = ObjectCreate ( null ) as Record < EventType , Handler >; 245for ( let i = 0 ; i < eventTypes . length ; i ++ ) { 246handlers [ eventTypes [ i ]] = { value :null , listener :null }; 247} 248set ( sockets , socket , { 249 id, 250href :urlHref ( record ), 251origin :urlOrigin ( record ), 252state :CONNECTING , 253protocol :"" , 254extensions :"" , 255binaryType :"blob" , 256buffered :0 , 257 handlers, 258}); 259markPromiseHandled ( pump ( id , socket )); 260return socket ; 261} 262 263function adoptWebSocket ( id :number , href :string ) :WebSocket { 264const socket = construct ( EventTarget , [], WebSocket ) as WebSocket ; 265return initializeSocket ( socket , id , new URL ( `ws ${ slice ( href , 4 )} ` )); 266} 267 268function handler ( type :EventType ) { 269return { 270get ( this :WebSocket ) :Function | null { 271return state ( sockets , this ). handlers [ type ]. value ; 272}, 273set ( this :WebSocket , rawValue :unknown ) :void { 274const handler = state ( sockets , this ). handlers [ type ]; 275const value = typeof rawValue === "function" ?rawValue :null ; 276handler . value = value ; 277if ( value && ! handler . listener ) { 278handler . listener = ( event ) => { 279if ( apply ( handler . value ! , this , [ event ]) === false ) { 280preventDefault ( event ); 281} 282}; 283addListener ( this , type , handler . listener ); 284} else if ( ! value && handler . listener ) { 285removeListener ( this , type , handler . listener ); 286handler . listener = null ; 287} 288}, 289configurable :true , 290}; 291} 292 293class CloseEvent extends Event { 294// @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super() call. 295constructor ( type :unknown , init :unknown = {}) { 296required ( arguments . length , 1 ); 297type = string ( type ); 298const options = dictionary ( init ); 299const self = construct ( Event , [ type , options ], new . target ) as CloseEvent ; 300const rawCode = options . code , 301reason = options . reason ; 302set ( closeEvents , self , { 303wasClean :!! options . wasClean , 304code :rawCode === undefined ?0 :toUint16 ( rawCode ), 305reason :reason === undefined ?"" :usv ( reason ), 306}); 307return self ; 308} 309get wasClean () { 310return state ( closeEvents , this ). wasClean ; 311} 312get code () { 313return state ( closeEvents , this ). code ; 314} 315get reason () { 316return state ( closeEvents , this ). reason ; 317} 318} 319 320interface WebSocket { 321readonly CONNECTING :typeof CONNECTING ; 322readonly OPEN :typeof OPEN ; 323readonly CLOSING :typeof CLOSING ; 324readonly CLOSED :typeof CLOSED ; 325onopen :EventHandler < Event > | null ; 326onmessage :EventHandler < MessageEvent > | null ; 327onerror :EventHandler < Event > | null ; 328onclose :EventHandler < CloseEvent > | null ; 329} 330class WebSocket extends EventTarget { 331declare static readonly CONNECTING :typeof CONNECTING ; 332declare static readonly OPEN :typeof OPEN ; 333declare static readonly CLOSING :typeof CLOSING ; 334declare static readonly CLOSED :typeof CLOSED ; 335 336// @ts-expect-error Reflect.construct preserves new.target without a guest-replaceable super() call. 337constructor ( url :unknown , protocols :unknown = undefined ) { 338required ( arguments . length , 1 ); 339const self = construct ( EventTarget , [], new . target ) as WebSocket ; 340url = usv ( url ); 341const list = convertProtocols ( protocols ); 342let record ; 343try { 344record = new URL ( url ); 345} catch { 346throw syntaxError ( "The URL is invalid" ); 347} 348const protocol = urlProtocol ( record ); 349if ( protocol === "http:" ) { 350record = new URL ( `ws ${ slice ( urlHref ( record ), 4 )} ` ); 351} else if ( protocol === "https:" ) { 352record = new URL ( `wss ${ slice ( urlHref ( record ), 5 )} ` ); 353} else if ( protocol !== "ws:" && protocol !== "wss:" ) { 354throw syntaxError ( "The URL's scheme must be ws, wss, http, or https" ); 355} 356const href = urlHref ( record ); 357if ( includes ( href , "#" )) { 358throw syntaxError ( "The URL contains a fragment" ); 359} 360const seen = new Set < string >(); 361for ( let i = 0 ; i < list . length ; i ++ ) { 362const item = list [ i ]; 363if ( item === "" || exec ( invalidToken , item ) !== null ) { 364throw syntaxError ( "Invalid WebSocket subprotocol" ); 365} 366if ( has ( seen , item )) { 367throw syntaxError ( "Duplicate WebSocket subprotocol" ); 368} 369add ( seen , item ); 370} 371// Permission failures surface synchronously, as in fetch. 372const id = op_ws_create ([ href , list , userAgent ]); 373return initializeSocket ( self , id , record ); 374} 375get url () { 376return state ( sockets , this ). href ; 377} 378get readyState () { 379return state ( sockets , this ). state ; 380} 381get bufferedAmount () { 382return state ( sockets , this ). buffered ; 383} 384get extensions () { 385return state ( sockets , this ). extensions ; 386} 387get protocol () { 388return state ( sockets , this ). protocol ; 389} 390get binaryType () :"blob" | "arraybuffer" { 391return state ( sockets , this ). binaryType ; 392} 393set binaryType ( value :unknown ) { 394const s = state ( sockets , this ), binaryType = string ( value ); 395if ( binaryType === "blob" || binaryType === "arraybuffer" ) { 396s . binaryType = binaryType ; 397} 398} 399close ( code :unknown = undefined , reason :unknown = undefined ) :void { 400const s = state ( sockets , this ); 401let closeCode :number | null = null ; 402if ( code !== undefined ) closeCode = clamp ( code ); 403if ( reason !== undefined ) reason = usv ( reason ); 404if ( 405closeCode !== null && closeCode !== 1000 && 406( closeCode < 3000 || closeCode > 4999 ) 407) { 408throw createException ({ 409message :"Invalid WebSocket close code" , 410name :"InvalidAccessError" , 411}); 412} 413if ( reason !== undefined && length ( encode ( encoder , reason )) > 123 ) { 414throw syntaxError ( "The WebSocket close reason is too long" ); 415} 416if ( s . state === CLOSING || s . state === CLOSED ) return ; 417if ( s . state === CONNECTING ) { 418// Failing the connection instead of starting the closing handshake. 419s . state = CLOSING ; 420op_ws_fail ( s . id ); 421return ; 422} 423s . state = CLOSING ; 424op_ws_close ([ s . id , closeCode , ( reason as string | undefined ) ?? null ]); 425} 426send ( data :unknown ) :void { 427const s = state ( sockets , this ); 428required ( arguments . length , 1 ); 429let text = true , 430bytes :Uint8Array < ArrayBuffer >; 431const blob = blobBody ( data ); 432if ( blob !== null ) { 433bytes = blob . bytes ; 434text = false ; 435} else if ( isBufferSource ( data )) { 436const b = isView ( data ) 437 ?tag ( data ) === undefined ?dataViewBuffer ( data ) :buffer ( data ) 438 :data ; 439// The getter's brand check also rejects SharedArrayBuffers, which BufferSource excludes. 440if ( resizable ( b )) { 441throw new TypeError ( "WebSocket data must have a fixed buffer" ); 442} 443bytes = snapshot ( data ); 444text = false ; 445} else { 446// The IDL union's last member: everything else becomes a USVString. 447bytes = encode ( encoder , usv ( data )); 448} 449if ( s . state === CONNECTING ) { 450throw createException ({ 451message :"WebSocket is not connected" , 452name :"InvalidStateError" , 453}); 454} 455const size = length ( bytes ); 456s . buffered += size ; 457if ( s . state !== OPEN ) return ; 458markPromiseHandled ( 459then ( op_ws_send ([ s . id , text , bytes ]), ( sent ) => { 460if ( sent && s . state !== CLOSED ) s . buffered -= size ; 461}), 462); 463} 464} 465 466for ( 467const [ name , value ] of [ 468[ "CONNECTING" , 0 ], 469[ "OPEN" , 1 ], 470[ "CLOSING" , 2 ], 471[ "CLOSED" , 3 ], 472] as const 473) { 474define ( WebSocket , name , { value, enumerable :true }); 475define ( WebSocket . prototype , name , { value, enumerable :true }); 476} 477for ( const type of eventTypes ) { 478define ( WebSocket . prototype , `on ${ type } ` , handler ( type )); 479} 480 481export { adoptWebSocket , CloseEvent , WebSocket };