Split out of #225.
ios_async.acquire returns null when all 16 slots are leased. Every caller then answers NativeCallFailed:
const ticket = ios_async.acquire(A.pick_contact) orelse return poolFull();The header comment used to claim the page got "an explicit Busy error". It does not — no such code exists — and #225 corrected the comment rather than inventing the code, because the change is wider than that fix.
Why it is worth doing
The two are different facts about the world, and a page can act on the difference. NativeCallFailed says the call broke; nothing suggests trying again. A full pool says sixteen calls are already in flight and this one arrived at a bad moment, which is retryable — and, for a page making them in a loop, diagnostic.
It also matters to whoever reads a bug report. A picker that reports NativeCallFailed sends a reader looking for a fault in the picker.
What it touches
A Busy variant on bridge_error.BridgeError, and the poolFull helper in each module that leases a slot — about twelve of them:
bridge_mobile_audiorec, bridge_mobile_auth, bridge_mobile_auth_apple, bridge_mobile_bluetooth, bridge_mobile_calendar, bridge_mobile_contactpicker, bridge_mobile_contacts, bridge_mobile_display, bridge_mobile_filepicker, bridge_mobile_imagepicker, and the others grep -l 'ios_async.acquire' packages/zig/src finds.
It is a behaviour change: a page catching on error.code === 'NATIVE_CALL_FAILED' for a full pool would stop matching. Worth checking craft.d.ts and the error-code docs in the same pass, since a new code is part of the public contract.
Found while fixing #225.