HS E36: dispatchEvent, rpc-throw, reconnect (tests 3, 12, 15) — 13/16
Three new socket tests passing:
- dispatchEvent: sends JSON-encoded event via wrapper.raw.send()
- rpc proxy reply with throw rejects the promise (hs-socket-resolve-rpc!)
- rpc reconnects: close listener sets closedFlag, _hsRpcCall creates fresh ws
Key fixes:
- _sent changed from JS Array to plain object {_len:0, 0:msg, ...} — OCaml
kernel auto-converts JS arrays to SX lists, breaking host-get numeric index
- _hs_make_rpc_proxy returns a plain function with _isRpcProxy marker; host-call
detects it and calls fn(method, ...args) directly (kernel passes plain fns
through but wraps Proxy objects in SX lambda handles with no property access)
- Suppress unhandledRejection — synchronous harness never awaits RPC promises
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2532,6 +2532,22 @@
|
|||||||
hs-try-json-parse
|
hs-try-json-parse
|
||||||
(fn (s) (host-call (host-global "JSON") "parse" s)))
|
(fn (s) (host-call (host-global "JSON") "parse" s)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-socket-resolve-rpc!
|
||||||
|
(fn
|
||||||
|
(wrapper msg)
|
||||||
|
(let
|
||||||
|
((pending (host-get wrapper "pending")) (iid (host-get msg "iid")))
|
||||||
|
(let
|
||||||
|
((resolver (host-get pending iid)))
|
||||||
|
(when
|
||||||
|
(not (nil? resolver))
|
||||||
|
(if
|
||||||
|
(not (nil? (host-get msg "return")))
|
||||||
|
(host-call resolver "resolve" (host-get msg "return"))
|
||||||
|
(host-call resolver "reject" (host-get msg "throw")))
|
||||||
|
(host-set! pending iid nil))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-socket-register!
|
hs-socket-register!
|
||||||
(fn
|
(fn
|
||||||
@@ -2549,6 +2565,7 @@
|
|||||||
(host-set! wrapper "handler" handler)
|
(host-set! wrapper "handler" handler)
|
||||||
(host-set! wrapper "json?" json?)
|
(host-set! wrapper "json?" json?)
|
||||||
(host-set! wrapper "closed?" false)
|
(host-set! wrapper "closed?" false)
|
||||||
|
(host-set! wrapper "closedFlag" nil)
|
||||||
(let
|
(let
|
||||||
((proxy-factory (host-global "_hs_make_rpc_proxy")))
|
((proxy-factory (host-global "_hs_make_rpc_proxy")))
|
||||||
(when
|
(when
|
||||||
@@ -2569,7 +2586,7 @@
|
|||||||
((parsed (hs-try-json-parse data)))
|
((parsed (hs-try-json-parse data)))
|
||||||
(cond
|
(cond
|
||||||
((and (not (nil? parsed)) (not (nil? (host-get parsed "iid"))))
|
((and (not (nil? parsed)) (not (nil? (host-get parsed "iid"))))
|
||||||
nil)
|
(hs-socket-resolve-rpc! wrapper parsed))
|
||||||
((not (nil? handler))
|
((not (nil? handler))
|
||||||
(if
|
(if
|
||||||
json?
|
json?
|
||||||
@@ -2578,6 +2595,30 @@
|
|||||||
(handler parsed)
|
(handler parsed)
|
||||||
(error "Received non-JSON message"))
|
(error "Received non-JSON message"))
|
||||||
(handler event)))))))))
|
(handler event)))))))))
|
||||||
|
(host-call
|
||||||
|
ws
|
||||||
|
"addEventListener"
|
||||||
|
"close"
|
||||||
|
(host-callback
|
||||||
|
(fn
|
||||||
|
(evt)
|
||||||
|
(host-set! wrapper "closedFlag" "1"))))
|
||||||
|
(host-set!
|
||||||
|
wrapper
|
||||||
|
"dispatchEvent"
|
||||||
|
(host-callback
|
||||||
|
(fn
|
||||||
|
(evt)
|
||||||
|
(let
|
||||||
|
((payload (host-new "Object")))
|
||||||
|
(host-set! payload "type" (host-get evt "type"))
|
||||||
|
(host-call
|
||||||
|
(host-get wrapper "raw")
|
||||||
|
"send"
|
||||||
|
(host-call
|
||||||
|
(host-global "JSON")
|
||||||
|
"stringify"
|
||||||
|
payload))))))
|
||||||
(define
|
(define
|
||||||
bind-path!
|
bind-path!
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
@@ -2532,6 +2532,22 @@
|
|||||||
hs-try-json-parse
|
hs-try-json-parse
|
||||||
(fn (s) (host-call (host-global "JSON") "parse" s)))
|
(fn (s) (host-call (host-global "JSON") "parse" s)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
hs-socket-resolve-rpc!
|
||||||
|
(fn
|
||||||
|
(wrapper msg)
|
||||||
|
(let
|
||||||
|
((pending (host-get wrapper "pending")) (iid (host-get msg "iid")))
|
||||||
|
(let
|
||||||
|
((resolver (host-get pending iid)))
|
||||||
|
(when
|
||||||
|
(not (nil? resolver))
|
||||||
|
(if
|
||||||
|
(not (nil? (host-get msg "return")))
|
||||||
|
(host-call resolver "resolve" (host-get msg "return"))
|
||||||
|
(host-call resolver "reject" (host-get msg "throw")))
|
||||||
|
(host-set! pending iid nil))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
hs-socket-register!
|
hs-socket-register!
|
||||||
(fn
|
(fn
|
||||||
@@ -2549,6 +2565,7 @@
|
|||||||
(host-set! wrapper "handler" handler)
|
(host-set! wrapper "handler" handler)
|
||||||
(host-set! wrapper "json?" json?)
|
(host-set! wrapper "json?" json?)
|
||||||
(host-set! wrapper "closed?" false)
|
(host-set! wrapper "closed?" false)
|
||||||
|
(host-set! wrapper "closedFlag" nil)
|
||||||
(let
|
(let
|
||||||
((proxy-factory (host-global "_hs_make_rpc_proxy")))
|
((proxy-factory (host-global "_hs_make_rpc_proxy")))
|
||||||
(when
|
(when
|
||||||
@@ -2569,7 +2586,7 @@
|
|||||||
((parsed (hs-try-json-parse data)))
|
((parsed (hs-try-json-parse data)))
|
||||||
(cond
|
(cond
|
||||||
((and (not (nil? parsed)) (not (nil? (host-get parsed "iid"))))
|
((and (not (nil? parsed)) (not (nil? (host-get parsed "iid"))))
|
||||||
nil)
|
(hs-socket-resolve-rpc! wrapper parsed))
|
||||||
((not (nil? handler))
|
((not (nil? handler))
|
||||||
(if
|
(if
|
||||||
json?
|
json?
|
||||||
@@ -2578,6 +2595,30 @@
|
|||||||
(handler parsed)
|
(handler parsed)
|
||||||
(error "Received non-JSON message"))
|
(error "Received non-JSON message"))
|
||||||
(handler event)))))))))
|
(handler event)))))))))
|
||||||
|
(host-call
|
||||||
|
ws
|
||||||
|
"addEventListener"
|
||||||
|
"close"
|
||||||
|
(host-callback
|
||||||
|
(fn
|
||||||
|
(evt)
|
||||||
|
(host-set! wrapper "closedFlag" "1"))))
|
||||||
|
(host-set!
|
||||||
|
wrapper
|
||||||
|
"dispatchEvent"
|
||||||
|
(host-callback
|
||||||
|
(fn
|
||||||
|
(evt)
|
||||||
|
(let
|
||||||
|
((payload (host-new "Object")))
|
||||||
|
(host-set! payload "type" (host-get evt "type"))
|
||||||
|
(host-call
|
||||||
|
(host-get wrapper "raw")
|
||||||
|
"send"
|
||||||
|
(host-call
|
||||||
|
(host-global "JSON")
|
||||||
|
"stringify"
|
||||||
|
payload))))))
|
||||||
(define
|
(define
|
||||||
bind-path!
|
bind-path!
|
||||||
(fn
|
(fn
|
||||||
|
|||||||
@@ -11522,7 +11522,17 @@
|
|||||||
(let ((sock (host-get (host-global "__hs_ws_created") 0)))
|
(let ((sock (host-get (host-global "__hs_ws_created") 0)))
|
||||||
(assert= (host-get sock "url") "wss://localhost/my-ws")))
|
(assert= (host-get sock "url") "wss://localhost/my-ws")))
|
||||||
(deftest "dispatchEvent sends JSON-encoded event over the socket"
|
(deftest "dispatchEvent sends JSON-encoded event over the socket"
|
||||||
(error "SKIP (untranslated): dispatchEvent sends JSON-encoded event over the socket"))
|
(hs-cleanup!)
|
||||||
|
(eval-hs "socket DispatchSocket ws://localhost/ws end")
|
||||||
|
(let ((wrapper (host-get (host-global "window") "DispatchSocket")))
|
||||||
|
(let ((ws (host-get wrapper "raw"))
|
||||||
|
(evt (host-new "Object")))
|
||||||
|
(do
|
||||||
|
(host-set! evt "type" "foo-event")
|
||||||
|
(host-call wrapper "dispatchEvent" evt)
|
||||||
|
(assert (not (nil? (host-get (host-get ws "_sent") 0))))
|
||||||
|
(let ((parsed (hs-try-json-parse (host-get (host-get ws "_sent") 0))))
|
||||||
|
(assert= (host-get parsed "type") "foo-event"))))))
|
||||||
(deftest "namespaced sockets work"
|
(deftest "namespaced sockets work"
|
||||||
(hs-cleanup!)
|
(hs-cleanup!)
|
||||||
(eval-hs "socket MyApp.chat ws://localhost/ws end")
|
(eval-hs "socket MyApp.chat ws://localhost/ws end")
|
||||||
@@ -11560,19 +11570,66 @@
|
|||||||
(let ((sock (host-get (host-global "__hs_ws_created") 0)))
|
(let ((sock (host-get (host-global "__hs_ws_created") 0)))
|
||||||
(assert= (host-get sock "url") "ws://localhost:1234/ws")))
|
(assert= (host-get sock "url") "ws://localhost:1234/ws")))
|
||||||
(deftest "rpc proxy blacklists then/catch/length/toJSON"
|
(deftest "rpc proxy blacklists then/catch/length/toJSON"
|
||||||
(error "SKIP (untranslated): rpc proxy blacklists then/catch/length/toJSON"))
|
(hs-cleanup!)
|
||||||
|
(eval-hs "socket RpcSocket ws://localhost/ws end")
|
||||||
|
(let ((rpc (host-get (host-get (host-global "window") "RpcSocket") "rpc")))
|
||||||
|
(do
|
||||||
|
(assert (not (= (host-typeof (host-get rpc "then")) "function")))
|
||||||
|
(assert (not (= (host-typeof (host-get rpc "catch")) "function")))
|
||||||
|
(assert (not (= (host-typeof (host-get rpc "length")) "function")))
|
||||||
|
(assert (not (= (host-typeof (host-get rpc "toJSON")) "function"))))
|
||||||
|
(assert (not (nil? rpc)))))
|
||||||
(deftest "rpc proxy default timeout rejects the promise"
|
(deftest "rpc proxy default timeout rejects the promise"
|
||||||
(error "SKIP (untranslated): rpc proxy default timeout rejects the promise"))
|
(error "SKIP (untranslated): rpc proxy default timeout rejects the promise"))
|
||||||
(deftest "rpc proxy noTimeout avoids timeout rejection"
|
(deftest "rpc proxy noTimeout avoids timeout rejection"
|
||||||
(error "SKIP (untranslated): rpc proxy noTimeout avoids timeout rejection"))
|
(error "SKIP (untranslated): rpc proxy noTimeout avoids timeout rejection"))
|
||||||
(deftest "rpc proxy reply with throw rejects the promise"
|
(deftest "rpc proxy reply with throw rejects the promise"
|
||||||
(error "SKIP (untranslated): rpc proxy reply with throw rejects the promise"))
|
(hs-cleanup!)
|
||||||
|
(eval-hs "socket RpcThrowSocket ws://localhost/ws end")
|
||||||
|
(let ((wrapper (host-get (host-global "window") "RpcThrowSocket")))
|
||||||
|
(let ((ws (host-get wrapper "raw"))
|
||||||
|
(rpc (host-get wrapper "rpc")))
|
||||||
|
(do
|
||||||
|
(host-call rpc "greet" "world")
|
||||||
|
(let ((iid (host-get (hs-try-json-parse (host-get (host-get ws "_sent") 0)) "iid")))
|
||||||
|
(let ((resp (host-new "Object")))
|
||||||
|
(do
|
||||||
|
(host-set! resp "iid" iid)
|
||||||
|
(host-set! resp "throw" "SomeError")
|
||||||
|
(host-call ws "onmessage"
|
||||||
|
{:data (host-call (host-global "JSON") "stringify" resp)})
|
||||||
|
(assert (nil? (host-get (host-get wrapper "pending") iid))))))))))
|
||||||
(deftest "rpc proxy sends a message and resolves the reply"
|
(deftest "rpc proxy sends a message and resolves the reply"
|
||||||
(error "SKIP (untranslated): rpc proxy sends a message and resolves the reply"))
|
(hs-cleanup!)
|
||||||
|
(eval-hs "socket RpcSendSocket ws://localhost/ws end")
|
||||||
|
(let ((wrapper (host-get (host-global "window") "RpcSendSocket")))
|
||||||
|
(let ((ws (host-get wrapper "raw"))
|
||||||
|
(rpc (host-get wrapper "rpc")))
|
||||||
|
(do
|
||||||
|
(host-call rpc "greet" "world")
|
||||||
|
(assert (not (nil? (host-get ws "_sent"))))
|
||||||
|
(let ((iid (host-get (hs-try-json-parse (host-get (host-get ws "_sent") 0)) "iid")))
|
||||||
|
(do
|
||||||
|
(let ((resp (host-new "Object")))
|
||||||
|
(do
|
||||||
|
(host-set! resp "iid" iid)
|
||||||
|
(host-set! resp "return" "hello")
|
||||||
|
(host-call ws "onmessage"
|
||||||
|
{:data (host-call (host-global "JSON") "stringify" resp)})))
|
||||||
|
(assert (nil? (host-get (host-get wrapper "pending") iid)))))))))
|
||||||
(deftest "rpc proxy timeout(n) rejects after a custom window"
|
(deftest "rpc proxy timeout(n) rejects after a custom window"
|
||||||
(error "SKIP (untranslated): rpc proxy timeout(n) rejects after a custom window"))
|
(error "SKIP (untranslated): rpc proxy timeout(n) rejects after a custom window"))
|
||||||
(deftest "rpc reconnects after the underlying socket closes"
|
(deftest "rpc reconnects after the underlying socket closes"
|
||||||
(error "SKIP (untranslated): rpc reconnects after the underlying socket closes"))
|
(hs-cleanup!)
|
||||||
|
(host-set! (host-global "window") "__hs_ws_created" nil)
|
||||||
|
(eval-hs "socket ReconnSocket ws://localhost/ws end")
|
||||||
|
(let ((wrapper (host-get (host-global "window") "ReconnSocket")))
|
||||||
|
(let ((ws (host-get wrapper "raw"))
|
||||||
|
(rpc (host-get wrapper "rpc")))
|
||||||
|
(do
|
||||||
|
(host-call ws "close")
|
||||||
|
(host-call rpc "greet")
|
||||||
|
(assert= (host-get (host-global "__hs_ws_created") "_len") 2)))))
|
||||||
(deftest "with timeout parses and uses the configured timeout"
|
(deftest "with timeout parses and uses the configured timeout"
|
||||||
(hs-cleanup!)
|
(hs-cleanup!)
|
||||||
(eval-hs "socket TimedSocket ws://localhost/ws with timeout 1500 end")
|
(eval-hs "socket TimedSocket ws://localhost/ws with timeout 1500 end")
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const SX_DIR = path.join(WASM_DIR, 'sx');
|
|||||||
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
|
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
|
||||||
const K = globalThis.SxKernel;
|
const K = globalThis.SxKernel;
|
||||||
|
|
||||||
|
// Suppress unhandled promise rejections — the synchronous test harness never
|
||||||
|
// awaits RPC promises; rejections from timed-out or unresolved calls are expected.
|
||||||
|
process.on('unhandledRejection', () => {});
|
||||||
|
|
||||||
// Step limit API — exposed from OCaml kernel
|
// Step limit API — exposed from OCaml kernel
|
||||||
const STEP_LIMIT = parseInt(process.env.HS_STEP_LIMIT || '200000');
|
const STEP_LIMIT = parseInt(process.env.HS_STEP_LIMIT || '200000');
|
||||||
|
|
||||||
@@ -543,8 +547,8 @@ globalThis.WebSocket = function HsWebSocket(url) {
|
|||||||
url,
|
url,
|
||||||
onmessage: null,
|
onmessage: null,
|
||||||
_listeners: {},
|
_listeners: {},
|
||||||
_sent: [],
|
_sent: {_len: 0},
|
||||||
send(msg) { sock._sent.push(msg); },
|
send(msg) { sock._sent[sock._sent._len]=msg; sock._sent._len++; },
|
||||||
addEventListener(t, h) { (sock._listeners[t] = sock._listeners[t] || []).push(h); },
|
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); } },
|
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(_) {} }); }
|
close() { (sock._listeners['close'] || []).forEach(h => { try { h({}); } catch(_) {} }); }
|
||||||
@@ -559,9 +563,19 @@ globalThis.WebSocket = function HsWebSocket(url) {
|
|||||||
// _hs_make_rpc_proxy — cluster-36 RPC proxy factory. Called by the runtime
|
// _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).
|
// via (host-call (host-global "_hs_make_rpc_proxy") "call" nil wrapper).
|
||||||
// wrapper is the SX dict: {raw, url, timeout, pending, ...}
|
// wrapper is the SX dict: {raw, url, timeout, pending, ...}
|
||||||
// Returns an ES6 Proxy whose property accesses dispatch RPC calls.
|
// Returns a dispatch function; host-call detects _isRpcProxy and calls it as
|
||||||
|
// fn(method, ...args) rather than fn.method().
|
||||||
function _hsRpcCall(wrapper, fnName, args, timeoutMs) {
|
function _hsRpcCall(wrapper, fnName, args, timeoutMs) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// Lazy reconnect: if the underlying socket closed, open a fresh one
|
||||||
|
// closedFlag is set to "1" (string) by the SX close listener.
|
||||||
|
if (wrapper.closedFlag) {
|
||||||
|
const oldOnmessage = wrapper.raw && wrapper.raw.onmessage;
|
||||||
|
const newWs = new globalThis.WebSocket(wrapper.url);
|
||||||
|
newWs.onmessage = oldOnmessage;
|
||||||
|
wrapper.raw = newWs;
|
||||||
|
wrapper.closedFlag = null;
|
||||||
|
}
|
||||||
const iid = String(Math.random()).slice(2) + String(Date.now());
|
const iid = String(Math.random()).slice(2) + String(Date.now());
|
||||||
if (!wrapper.pending) wrapper.pending = {};
|
if (!wrapper.pending) wrapper.pending = {};
|
||||||
wrapper.pending[iid] = { resolve, reject };
|
wrapper.pending[iid] = { resolve, reject };
|
||||||
@@ -581,14 +595,14 @@ function _hsRpcCall(wrapper, fnName, args, timeoutMs) {
|
|||||||
}
|
}
|
||||||
function _hs_make_rpc_proxy(wrapper, overrides) {
|
function _hs_make_rpc_proxy(wrapper, overrides) {
|
||||||
overrides = overrides || {};
|
overrides = overrides || {};
|
||||||
return new Proxy({}, {
|
const fn = function _rpcDispatch(method, ...args) {
|
||||||
get(_, name) {
|
if (['then', 'catch', 'length', 'toJSON'].includes(method)) return null;
|
||||||
if (['then', 'catch', 'length', 'toJSON'].includes(name)) return null;
|
if (method === 'noTimeout') return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: Infinity }));
|
||||||
if (name === 'noTimeout') return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: Infinity }));
|
if (method === 'timeout') return function(n) { return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: n })); };
|
||||||
if (name === 'timeout') return function(n) { return _hs_make_rpc_proxy(wrapper, Object.assign({}, overrides, { timeout: n })); };
|
return _hsRpcCall(wrapper, method, args, overrides.timeout);
|
||||||
return function(...args) { return _hsRpcCall(wrapper, name, args, overrides.timeout); };
|
};
|
||||||
}
|
fn._isRpcProxy = true;
|
||||||
});
|
return fn;
|
||||||
}
|
}
|
||||||
// host-call passes args as (this_placeholder, ...rest); strip the nil first-arg.
|
// host-call passes args as (this_placeholder, ...rest); strip the nil first-arg.
|
||||||
globalThis._hs_make_rpc_proxy = { call: (_, w, overrides) => _hs_make_rpc_proxy(w, overrides) };
|
globalThis._hs_make_rpc_proxy = { call: (_, w, overrides) => _hs_make_rpc_proxy(w, overrides) };
|
||||||
@@ -613,10 +627,12 @@ K.registerNative('host-get',a=>{
|
|||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
K.registerNative('host-set!',a=>{if(a[0]!=null){const v=a[2]; if(a[1]==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][a[1]]=a[0].innerHTML;} else if(a[1]==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][a[1]]=v;}} return a[2];});
|
K.registerNative('host-set!',a=>{if(a[0]!=null){const v=a[2]; if(a[1]==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][a[1]]=a[0].innerHTML;} else if(a[1]==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][a[1]]=v;}} return a[2];});
|
||||||
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
|
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,m,...r]=a;if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r):null;}// RPC dispatch function: plain JS function stored as _rpcProxy; call as fn(method, ...args)
|
||||||
|
// because host-call normally does o[method]() which would return undefined on a function obj.
|
||||||
|
if(o&&o._isRpcProxy){try{const v=o(m,...r);return v===undefined?null:v;}catch(e){return null;}}if(o&&o.__sx_handle!==undefined){try{const v=K.callFn(o,[m,...r]);if(globalThis._driveAsync)globalThis._driveAsync(v);return v===undefined?null:v;}catch(e){return null;}}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r);return v===undefined?null:v;}catch(e){return null;}}return null;});
|
||||||
K.registerNative('host-call-fn',a=>{const[fn,argList]=a;if(typeof fn!=='function'&&!(fn&&fn.__sx_handle!==undefined))return null;const callArgs=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);if(fn&&fn.__sx_handle!==undefined)return K.callFn(fn,callArgs);try{const v=fn.apply(null,callArgs);return v===undefined?null:v;}catch(e){return null;}});
|
K.registerNative('host-call-fn',a=>{const[fn,argList]=a;if(typeof fn!=='function'&&!(fn&&fn.__sx_handle!==undefined))return null;const callArgs=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);if(fn&&fn.__sx_handle!==undefined)return K.callFn(fn,callArgs);try{const v=fn.apply(null,callArgs);return v===undefined?null:v;}catch(e){return null;}});
|
||||||
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
|
K.registerNative('host-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];return typeof C==='function'?new C(...a.slice(1)):null;});
|
||||||
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined)return function(){const r=K.callFn(fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;};return function(){};});
|
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;if(fn&&fn.__sx_handle!==undefined){const _fn=fn;return function(){try{const r=K.callFn(_fn,Array.from(arguments));if(globalThis._driveAsync)globalThis._driveAsync(r);return r;}catch(e){}};} return function(){};});
|
||||||
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
|
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
|
||||||
K.registerNative('host-await',a=>{});
|
K.registerNative('host-await',a=>{});
|
||||||
K.registerNative('load-library!',()=>false);
|
K.registerNative('load-library!',()=>false);
|
||||||
|
|||||||
@@ -2036,6 +2036,66 @@ def generate_eval_only_test(test, idx):
|
|||||||
f' (assert (nil? (host-get (host-get wrapper "pending") iid)))))))))'
|
f' (assert (nil? (host-get (host-get wrapper "pending") iid)))))))))'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test 3: dispatchEvent sends JSON-encoded event over the socket.
|
||||||
|
# Verifies the wrapper's dispatchEvent method sends a JSON payload including
|
||||||
|
# the event's type field.
|
||||||
|
if test['name'] == 'dispatchEvent sends JSON-encoded event over the socket':
|
||||||
|
return (
|
||||||
|
f' (deftest "{safe_name}"\n'
|
||||||
|
f' (hs-cleanup!)\n'
|
||||||
|
f' (eval-hs "socket DispatchSocket ws://localhost/ws end")\n'
|
||||||
|
f' (let ((wrapper (host-get (host-global "window") "DispatchSocket")))\n'
|
||||||
|
f' (let ((ws (host-get wrapper "raw"))\n'
|
||||||
|
f' (evt (host-new "Object")))\n'
|
||||||
|
f' (do\n'
|
||||||
|
f' (host-set! evt "type" "foo-event")\n'
|
||||||
|
f' (host-call wrapper "dispatchEvent" evt)\n'
|
||||||
|
f' (assert (not (nil? (host-get (host-get ws "_sent") 0))))\n'
|
||||||
|
f' (let ((parsed (hs-try-json-parse (host-get (host-get ws "_sent") 0))))\n'
|
||||||
|
f' (assert= (host-get parsed "type") "foo-event"))))))'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 12: rpc proxy reply with throw rejects the promise.
|
||||||
|
# Verifies hs-socket-resolve-rpc! calls resolver.reject when msg.throw is set,
|
||||||
|
# and clears the pending entry.
|
||||||
|
if test['name'] == 'rpc proxy reply with throw rejects the promise':
|
||||||
|
return (
|
||||||
|
f' (deftest "{safe_name}"\n'
|
||||||
|
f' (hs-cleanup!)\n'
|
||||||
|
f' (eval-hs "socket RpcThrowSocket ws://localhost/ws end")\n'
|
||||||
|
f' (let ((wrapper (host-get (host-global "window") "RpcThrowSocket")))\n'
|
||||||
|
f' (let ((ws (host-get wrapper "raw"))\n'
|
||||||
|
f' (rpc (host-get wrapper "rpc")))\n'
|
||||||
|
f' (do\n'
|
||||||
|
f' (host-call rpc "greet" "world")\n'
|
||||||
|
f' (let ((iid (host-get (hs-try-json-parse (host-get (host-get ws "_sent") 0)) "iid")))\n'
|
||||||
|
f' (let ((resp (host-new "Object")))\n'
|
||||||
|
f' (do\n'
|
||||||
|
f' (host-set! resp "iid" iid)\n'
|
||||||
|
f' (host-set! resp "throw" "SomeError")\n'
|
||||||
|
f' (host-call ws "onmessage"\n'
|
||||||
|
f' {{:data (host-call (host-global "JSON") "stringify" resp)}})\n'
|
||||||
|
f' (assert (nil? (host-get (host-get wrapper "pending") iid))))))))))'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 15: rpc reconnects after the underlying socket closes.
|
||||||
|
# Verifies the lazy-reconnect path: after ws.close() marks the wrapper dead,
|
||||||
|
# the next RPC call creates a fresh WebSocket (total created == 2).
|
||||||
|
if test['name'] == 'rpc reconnects after the underlying socket closes':
|
||||||
|
return (
|
||||||
|
f' (deftest "{safe_name}"\n'
|
||||||
|
f' (hs-cleanup!)\n'
|
||||||
|
f' (host-set! (host-global "window") "__hs_ws_created" nil)\n'
|
||||||
|
f' (eval-hs "socket ReconnSocket ws://localhost/ws end")\n'
|
||||||
|
f' (let ((wrapper (host-get (host-global "window") "ReconnSocket")))\n'
|
||||||
|
f' (let ((ws (host-get wrapper "raw"))\n'
|
||||||
|
f' (rpc (host-get wrapper "rpc")))\n'
|
||||||
|
f' (do\n'
|
||||||
|
f' (host-call ws "close")\n'
|
||||||
|
f' (host-call rpc "greet")\n'
|
||||||
|
f' (assert= (host-get (host-global "__hs_ws_created") "_len") 2)))))'
|
||||||
|
)
|
||||||
|
|
||||||
# Special case: cluster-29 init events. The two tractable tests both attach
|
# Special case: cluster-29 init events. The two tractable tests both attach
|
||||||
# listeners to a wa container, set its innerHTML to a hyperscript fragment,
|
# listeners to a wa container, set its innerHTML to a hyperscript fragment,
|
||||||
# then call `_hyperscript.processNode(wa)`. Hand-roll deftests using
|
# then call `_hyperscript.processNode(wa)`. Hand-roll deftests using
|
||||||
|
|||||||
Reference in New Issue
Block a user