mirror of
https://github.com/logos-co/logos-view-module-runtime.git
synced 2026-08-30 20:41:14 +00:00
Both carried the same `if (!client->isConnected())` guard the event path did,
and QML issues calls from the same place it subscribes — Component.onCompleted,
the one moment the dependency's host has been spawned but has not called
listen(). The guard was dead code for years (isConnected() returned a latch that
was always true), so nothing exercised what it does when it goes live: it
returns {"error":"Module not connected"} to a view that will never retry, for
the life of the process.
Deleting the guard alone is not the fix either. Without it the call falls
through to the transport's default acquire budget — 20 s, paid twice because the
token handshake tries capability_module first — on the GUI thread. That is the
~417 s Basecamp stall the isConnected() fix exists to prevent.
The two forms owe their callers different things, so they get different answers:
* callModuleAsync() owes a CALLBACK, so it can wait. A module that is still
starting is no longer an error: the call is held via the protocol's new
whenObjectAvailable() and dispatched when the module appears, including one
installed mid-session. Nothing blocks. The deadline the caller already passes
(timeoutMs) now bounds the wait for the module as well as the call, so a
module that never appears still gets an answer instead of hanging politely.
* callModule() owes a RETURN VALUE now, so it cannot wait for a module at all.
It waits a short bounded time (1500 ms — QtRO retries its endpoint every
250 ms and a starting module arms in roughly 50-150 ms, so this covers the
real race several times over) and then answers. Its error now says "Module
not reachable yet" and names callModuleAsync, because for a view whose first
paint depends on the answer the async form is the correct tool, not a longer
timeout here.
The reachability question is asked once, ahead of both failure shapes: an absent
module surfaces as either an acquire CallError or an invalid result depending on
transport and timing, and inferring "still starting" from which shape came back
is what made a startup race look like a broken module.
tests/test_logos_qml_bridge_calls.cpp is new, and is the first coverage this
path has had in either direction. It pins the two contracts separately, drives
the async form through a real QJSEngine (a non-callable QJSValue is silently
ignored, so a C++ lambda would not exercise what QML does), and asserts the
SHAPE of each failure rather than just that one occurred — "some error payload"
is what the broken version returned too. With the guards restored, three cases
go red, two of them reporting the old {"error":"Module not connected"} verbatim.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>