import{browser,intervalfn,is_ws,on_destroy,to_readable,to_writable,unstore,}from"@sveu/shared"importtype{Fn}from"@sveu/shared"import{on}from"../event_listener"importtype{WebSocketOptions,WebSocketReturn,WebSocketStatus,}from"../utils"constDEFAULT_PING_MESSAGE="ping"functionresolve_nested_options<T>(options:T|true):T{if(options===true)return{}asTreturnoptions}/** * Reactive WebSocket client. * * @param url - The websocket url. * * @param options - The websocket options. * * @returns * - `data`: The data received from the websocket server. * - `status`: The current websocket status, can be only one of: 'OPEN', 'CONNECTING', 'CLOSED' * - `ws`: Reference to the WebSocket instance. * - `close`: Closes the websocket connection gracefully. * - `open`: Reopen the websocket connection. If there the current one is active, will close it before opening a new one. * - `send`: Sends data through the websocket connection. */exportfunctionwebsocket<T>(url:string,options:WebSocketOptions={}):WebSocketReturn<T>{const{on_connected,on_disconnected,on_error,on_message,immediate=true,auto_close=true,protocols=[],}=optionsconstdata=to_writable<T|null>(null)conststatus=to_writable<WebSocketStatus>("CLOSED")constws_store=to_writable<WebSocket|undefined>(undefined)letheartbeat_pause:Fn|undefinedletheartbeat_resume:Fn|undefinedletexplicitly_closed=falseletretried=0letbuffered_data:(string|ArrayBuffer|Blob)[]=[]letpong_timeout_wait:ReturnType<typeofsetTimeout>|undefined/** * Close the websocket connection. * * @param code - The code of the close event. Default to `1000`. see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code * * @param reason - The reason of the close event. * */functionclose(code=1000,reason?:string){if(!unstore(ws_store))returnexplicitly_closed=trueheartbeat_pause?.()unstore(ws_store)?.close(code,reason)}function_send_buffer(){if(buffered_data?.length&&unstore(ws_store)&&unstore(status)==="OPEN"){for(constbufferofbuffered_data)unstore(ws_store)?.send(buffer)buffered_data=[]}}functionreset_heartbeat(){clearTimeout(pong_timeout_wait)pong_timeout_wait=undefined}/** * Send data to the websocket server. * * @param data - The data to send. * * @param buffer - Whether to buffer the data if the websocket is not connected. Default to `true`. * * @returns Whether the data is sent. */functionsend(data:string|ArrayBuffer|Blob,buffer=true){if(!unstore(ws_store)||unstore(status)!=="OPEN"){if(buffer)buffered_data=[...buffered_data,data]returnfalse}_send_buffer()unstore(ws_store)?.send(data)returntrue}function_init(){if(explicitly_closed)returnconstws=newWebSocket(url,protocols)ws_store.set(ws)status.set("CONNECTING")ws.onopen=()=>{status.set("OPEN")on_connected?.(ws)heartbeat_resume?.()_send_buffer()}ws.onclose=(event:CloseEvent)=>{status.set("CLOSED")ws_store.set(undefined)on_disconnected?.(ws,event)if(!explicitly_closed&&options.auto_reconnect){const{retries=-1,delay=1,on_failed,}=resolve_nested_options(options.auto_reconnect)retried+=1if(typeofretries==="number"&&(retries<0||retried<retries))setTimeout(_init,delay*1000)elseif(typeofretries==="function"&&retries())setTimeout(_init,delay*1000)elseon_failed?.()}}ws.onerror=(event)=>{on_error?.(ws,event)}ws.onmessage=(event:MessageEvent<any>)=>{if(options.heartbeat){reset_heartbeat()const{message=DEFAULT_PING_MESSAGE}=resolve_nested_options(options.heartbeat)if(event.data===message)return}data.set(event.data)on_message?.(ws,event)}}if(options.heartbeat){const{message=DEFAULT_PING_MESSAGE,interval=1,pong_timeout=1,}=resolve_nested_options(options.heartbeat)const{pause,resume}=intervalfn(()=>{send(message,false)pong_timeout_wait=setTimeout(()=>{// auto-reconnect will be trigger with ws.onclose()close()},pong_timeout*1000)},interval,{immediate:false})heartbeat_pause=pauseheartbeat_resume=resume}if(immediate&&is_ws)_init()if(auto_close){if(browser)on(window,"beforeunload",()=>close())on_destroy(close)}functionopen(){close()explicitly_closed=falseretried=0_init()}return{data:to_readable(data),status:to_readable(status),ws:to_readable(ws_store),close,send,open,}}