framerock/frontend/index.js
2025-08-12 22:47:53 +00:00

239 lines
5.6 KiB
JavaScript

// 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()
}