diff --git a/src/audio/addon/AddonContext.h b/src/audio/addon/AddonContext.h index fec85f7..5f0ca2d 100644 --- a/src/audio/addon/AddonContext.h +++ b/src/audio/addon/AddonContext.h @@ -80,6 +80,14 @@ inline bool dispatchOnMessageThread(Func&& func) // closure must own everything it touches — capture by value (engine // snapshot, args) and write results through a shared_ptr, never through // 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 inline bool runDeviceLifecycleOp(Func&& func) { diff --git a/src/audio/addon/DeviceBindings.cpp b/src/audio/addon/DeviceBindings.cpp index 420e209..47457ca 100644 --- a/src/audio/addon/DeviceBindings.cpp +++ b/src/audio/addon/DeviceBindings.cpp @@ -126,9 +126,24 @@ Napi::Value ProbeDeviceOptions(const Napi::CallbackInfo& info) return obj; } - auto options = liveEngine->probeDeviceOptionsDual( - juce::String(inputType), juce::String(inputName), - juce::String(outputType), juce::String(outputName)); + // Probing constructs + destroys short-lived ASIO device objects + // (DeviceSetup::probeDual / rateSupportedBy createDevice) — same + // 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(); + 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("inputType", options.inputType.toStdString()); obj.Set("outputType", options.outputType.toStdString());