// provided by external "define": APP_CONSTANTS, JS_ON_WS_CONNECT const get_probably_unique_idstr_32len = function () { return (Date.now().toString() + Math.random().toString().replaceAll('.','')).padEnd(32, 'x').slice(0, 32) } const setup_safer_eval = function (lst_idents) { const runCodeWithCustomFunction = (obj) => { return Function(...lst_idents.map(x=>x[0]), `"use strict";return (${obj});`.trim())(...lst_idents.map(x=>x[1])) } return runCodeWithCustomFunction } const do_safereval_strscript = function (obj_eval_defines, the_strscript) { const lst_idents = Object.entries(obj_eval_defines).map(([k,v])=>[k,v]).toSorted((a,b)=>(a[0]-b[0])) const the_eval_func = setup_safer_eval(lst_idents) const the_eval_result = the_eval_func(` (function(){ ${the_strscript} return })() `.trim()) return the_eval_result } const render_app = function ({ APP_CONSTANTS, func_send_event, handle_event_response }) { const uidsub_map = {} const register_streamjoin = (stream_key, init_params, func_callback) => { const uid_sub = get_probably_unique_idstr_32len() console.info(['register_streamjoin', { stream_key, init_params, uid_sub }]) uidsub_map[uid_sub] = { func_callback } func_send_event(APP_CONSTANTS.EVENTTYPES.STREAM_JOIN, { uid_sub, stream_key, init_params }, (full_result) => { console.info(['STREAM_JOIN', { full_result }]) return }) const func_teardown = () => { func_send_event(APP_CONSTANTS.EVENTTYPES.STREAM_LEAVE, { uid_sub }, (full_result) => { console.info(['STREAM_LEAVE', { full_result }]) return }) delete uidsub_map[uid_sub] return } return func_teardown } const func_eval_appscript = () => { console.info('EVAL APPSCRIPT') do_safereval_strscript({ register_streamjoin }, JS_ON_WS_CONNECT) return } const func_handle_event = (evt_type, evt_data) => { //console.info(['HANDLING EVENT!', [ evt_type, evt_data ]]) if (evt_type === APP_CONSTANTS.EVENTTYPES.HBPING) { func_send_event(APP_CONSTANTS.EVENTTYPES.HBPONG, true) } else if (evt_type === APP_CONSTANTS.EVENTTYPES.EVENTPAIR_RESP) { handle_event_response(evt_data) } else if (evt_type === APP_CONSTANTS.EVENTTYPES.EVENTSUB_MSG) { const [ uid_sub, stream_data ] = evt_data const sub_info = uidsub_map[uid_sub] if (sub_info === undefined) { console.warn(['EVENTSUB_MSG but no handler', { uid_sub }]) } else { const { func_callback } = sub_info func_callback(stream_data.sub_msg, stream_data.is_final_message) // NOTE: automatic teardown if (stream_data.is_final_message === true) { console.info(['auto-teardown!', uid_sub]) delete uidsub_map[uid_sub] } } } else { console.warn(['unhandled evt_type', { evt_type }]) } return } const func_handle_wsopen = () => { func_eval_appscript() return } const func_handle_wsclose = () => { return } return { func_handle_event, func_handle_wsopen, func_handle_wsclose } } const async_main = async function () { let ref_renderapp const ws = new WebSocket(`ws://${window.location.host}/ws`) ws.binaryType = 'arraybuffer' const event_pair_map = {} const func_send_event = (subevt_type, subevt_data, func_callback) => { //console.info(['SEND EVENT', { subevt_type, subevt_data }]) let uid_envelope if (func_callback) { uid_envelope = get_probably_unique_idstr_32len() } else { // things might not "need" envelope/response uid_envelope = null } event_pair_map[uid_envelope] = { handle_response: (x) => { if (func_callback) { func_callback(x) } else { console.warn(['handle_response but no callback', { subevt_type, subevt_data }]) } // clean up self delete event_pair_map[uid_envelope] return }, } if (ws.readyState === WebSocket.OPEN) { ws.send(new TextEncoder().encode(JSON.stringify([ uid_envelope, subevt_type, subevt_data ]))) } else { console.warn(['ws readyState !== OPEN : dropping event send', { subevt_type, subevt_data }]) } return } const handle_event_response = (evt_data) => { //console.info(['HANDLE EVENT', { evt_data }]) const [ uid_envelope, result ] = evt_data const { handle_response } = event_pair_map[uid_envelope] if (handle_response) { handle_response(result) } else { console.warn(['missing ref to handle_response', { uid_envelope, result }]) } return } const func_on_open = () => { console.info('WS OPEN') // forward to App ref_renderapp && ref_renderapp.func_handle_wsopen() return } const func_on_close = () => { console.info('WS CLOSE') // forward to App ref_renderapp && ref_renderapp.func_handle_wsclose() return } const func_on_message = (e) => { try { const [ evt_type, evt_data ] = JSON.parse(new TextDecoder().decode(e.data)) //console.info(['MESSAGE', { evt_type, evt_data }]) // forward to App ref_renderapp && ref_renderapp.func_handle_event(evt_type, evt_data) } catch (err) { console.error(err) } return } const func_on_error = () => { console.info('WS ERROR') return } ws.addEventListener('open' , func_on_open) ws.addEventListener('close' , func_on_close) ws.addEventListener('message', func_on_message) ws.addEventListener('error' , func_on_error) ref_renderapp = render_app({ APP_CONSTANTS, func_send_event, handle_event_response }) return } let after_page_ready after_page_ready = async function () { async_main().then(()=>{}).catch(console.error) return } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', after_page_ready) } else { after_page_ready() }