fix(audio): route device probing through the message thread; document timeout desync

Review follow-up (PR #113, finding 1): ProbeDeviceOptions constructs and
destroys short-lived ASIO device objects (DeviceSetup::probeDual /
rateSupportedBy createDevice) on the Node thread - the same
create/destroy-off-the-message-thread pattern as the lifecycle ops, and a
probe ASIOAudioIODevice owns the same reset-timer machinery. Wrap it in
runDeviceLifecycleOp for consistency.

Finding 2 (timeout desync - op reported failed may still complete late)
is documented as a known limitation on the helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-16 23:45:29 +02:00
co-authored by Claude Fable 5
parent aa0bb9d173
commit 895d0af969
2 changed files with 26 additions and 3 deletions
+8
View File
@@ -80,6 +80,14 @@ inline bool dispatchOnMessageThread(Func&& func)
// closure must own everything it touches — capture by value (engine // closure must own everything it touches — capture by value (engine
// snapshot, args) and write results through a shared_ptr, never through // snapshot, args) and write results through a shared_ptr, never through
// references to the caller's stack. // references to the caller's stack.
//
// KNOWN LIMITATION (timeout desync): when the 15 s wait expires the binding
// reports failure to JS, but the closure may still complete afterwards —
// e.g. addSource returns -1 yet the source gets created, holding its input
// device until the next reconfigure. Memory-safe by the capture rule above,
// just not logically reconciled; only reachable with the message thread
// jammed > 15 s. Callers that care can re-query engine state (listSources,
// getCurrentDevice) after a reported failure.
template <typename Func> template <typename Func>
inline bool runDeviceLifecycleOp(Func&& func) inline bool runDeviceLifecycleOp(Func&& func)
{ {
+18 -3
View File
@@ -126,9 +126,24 @@ Napi::Value ProbeDeviceOptions(const Napi::CallbackInfo& info)
return obj; return obj;
} }
auto options = liveEngine->probeDeviceOptionsDual( // Probing constructs + destroys short-lived ASIO device objects
juce::String(inputType), juce::String(inputName), // (DeviceSetup::probeDual / rateSupportedBy createDevice) — same
juce::String(outputType), juce::String(outputName)); // create/destroy-off-the-message-thread pattern as the lifecycle ops, so
// it takes the same hop (PR #113 review finding 1). Query-only (never
// open()), but a probe ASIOAudioIODevice owns the same reset-timer
// machinery as the live one.
auto optionsPtr = std::make_shared<AudioEngine::DeviceOptions>();
if (!runDeviceLifecycleOp([liveEngine, inputType, inputName, outputType, outputName, optionsPtr] {
*optionsPtr = liveEngine->probeDeviceOptionsDual(
juce::String(inputType), juce::String(inputName),
juce::String(outputType), juce::String(outputName));
}))
{
obj.Set("error", "device probe did not complete (message thread unavailable or timed out)");
obj.Set("compatible", false);
return obj;
}
const auto& options = *optionsPtr;
obj.Set("type", options.inputType.toStdString()); // legacy alias obj.Set("type", options.inputType.toStdString()); // legacy alias
obj.Set("inputType", options.inputType.toStdString()); obj.Set("inputType", options.inputType.toStdString());
obj.Set("outputType", options.outputType.toStdString()); obj.Set("outputType", options.outputType.toStdString());