fix(load): open-settle gate — kicadOpenFileBusy probe + collab entry guards for the parked-open embind trap (indirect call signature mismatch) + deterministic collab-load-fuzz e2e

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137pGo8W7asomGUTRMB7RzM
This commit is contained in:
Gergő Törcsvári 2026-07-30 14:17:48 +02:00
commit a26ef4ebeb
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
11 changed files with 964 additions and 17 deletions

56
wasm/bindings/open_gate.h Normal file
View file

@ -0,0 +1,56 @@
/*
* Truthful "kicadOpenFile in flight" signal for the web shell.
*
* kicadOpenFile runs OpenProjectFiles under Asyncify: the embind call unwinds
* back to JS long before the load finishes, and the chain stays parked (and
* resumes, and parks again) across the whole multi-second load. Any bare
* embind entry that walks the model while that chain is parked mid-mutation
* (collab snapshot, presence bind) can virtual-dispatch through a half-built
* item and trap ("indirect call signature mismatch" same class as the wx
* dispatch interlock and the drift-trio #10b fiber-busy probe, but through a
* JS entry neither of those covers).
*
* The guard is RAII on the open's C++ stack frame: an Asyncify unwind does not
* run destructors and a rewind resumes past the constructor, so the count is
* held for the park's entire lifetime and drops exactly when OpenProjectFiles
* truly returns (the same primitive as wxWasmDispatchGuard). A trap escaping
* the open leaves the count stuck the JS poll times out and degrades.
*/
#pragma once
namespace pcbjam_open
{
inline int& busyCount()
{
static int s_count = 0;
return s_count;
}
struct BusyGuard
{
BusyGuard() { ++busyCount(); }
~BusyGuard() { --busyCount(); }
};
/** JS-pollable: is a kicadOpenFile chain still in flight (possibly parked)? */
inline bool busy()
{
return busyCount() > 0;
}
/**
* Test-only deterministic park (tests/kicad/collab-load-fuzz.spec.ts): with a
* nonzero value, kicadOpenFile Asyncify-parks for this many ms on entry and
* again after OpenProjectFiles returns busy guard held, model fully loaded.
* Natural in-load parks (thread-pool futex waits) are scheduler-dependent and
* never happen on a fast idle machine, so the guard would be untestable in CI
* without this window. 0 (the default) is a no-op in production.
*/
inline int& testParkMs()
{
static int s_ms = 0;
return s_ms;
}
} // namespace pcbjam_open