HS-prep: WebSocket + RPC proxy mock

This commit is contained in:
2026-04-25 18:49:52 +00:00
parent 912649c426
commit 6327c05ca6

View File

@@ -528,9 +528,62 @@ class HsIntersectionObserver {
}
globalThis.IntersectionObserver = HsIntersectionObserver;
globalThis.IntersectionObserverEntry = class {};
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:''};
globalThis.navigator={userAgent:'node'}; globalThis.location={href:'http://localhost/',pathname:'/',search:'',hash:'',protocol:'http:',host:'localhost',hostname:'localhost',port:''};
globalThis.history={pushState(){},replaceState(){},back(){},forward(){}};
globalThis.getSelection=()=>({toString:()=>(globalThis.__test_selection||'')});
// HsWebSocket — cluster-36 WebSocket mock. Records every constructed socket
// in globalThis.__hs_ws_created so tests can assert on URLs and sent frames.
// Tests may override globalThis.WebSocket before activating hyperscript.
globalThis.__hs_ws_created = [];
globalThis.WebSocket = function HsWebSocket(url) {
const sock = {
url,
onmessage: null,
_listeners: {},
_sent: [],
send(msg) { sock._sent.push(msg); },
addEventListener(t, h) { (sock._listeners[t] = sock._listeners[t] || []).push(h); },
removeEventListener(t, h) { const a = sock._listeners[t]; if (a) { const i = a.indexOf(h); if (i >= 0) a.splice(i, 1); } },
close() { (sock._listeners['close'] || []).forEach(h => { try { h({}); } catch(_) {} }); }
};
globalThis.__hs_ws_created.push(sock);
return sock;
};
// _hs_make_rpc_proxy — cluster-36 RPC proxy factory. Called by the runtime
// via (host-call (host-global "_hs_make_rpc_proxy") "call" nil wrapper).
// wrapper is the SX dict: {raw, url, timeout, pending, ...}
// Returns an ES6 Proxy whose property accesses dispatch RPC calls.
function _hsRpcCall(wrapper, fnName, args, timeoutMs) {
return new Promise((resolve, reject) => {
const iid = String(Math.random()).slice(2) + String(Date.now());
if (!wrapper.pending) wrapper.pending = {};
wrapper.pending[iid] = { resolve, reject };
const raw = wrapper.raw;
const msg = JSON.stringify({ iid, function: fnName, args });
raw.send(msg);
const ms = timeoutMs === undefined ? (typeof wrapper.timeout === 'number' ? wrapper.timeout : 0) : timeoutMs;
if (ms !== Infinity && typeof ms === 'number') {
setTimeout(() => {
if (wrapper.pending && wrapper.pending[iid]) {
delete wrapper.pending[iid];
reject('Timed out');
}
}, ms);
}
});
}
function _hs_make_rpc_proxy(wrapper, overrides) {
overrides = overrides || {};
return new Proxy({}, {
get(_, name) {
if (['then', 'catch', 'length', 'toJSON'].includes(name)) return null;
if (name === 'noTimeout') return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: Infinity }));
if (name === 'timeout') return function(n) { return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: n })); };
return function(...args) { return _hsRpcCall(wrapper, name, args, overrides.timeout); };
}
});
}
globalThis._hs_make_rpc_proxy = { call: _hs_make_rpc_proxy };
const _origLog = console.log;
globalThis.console = { log: () => {}, error: () => {}, warn: () => {}, info: () => {}, debug: () => {} }; // suppress ALL console noise
const _log = _origLog; // keep reference for our own output