ReviewOS

also looking at this

craft-native/craft

fix(ios): report async pool exhaustion as busy

#267
Merged glennmichael123 wants to merge codex/ios-busy-error-code into main
19 files +53 -65
packages/zig/src/bridge_mobile_contactpicker.zigmodified+11-9
Changes to packages/zig/src/bridge_mobile_contactpicker.zig
@@ -374,16 +374,14 @@ pub const ContactPickerBridge = struct {
374374 }
375375};
376376
377/// The answer for a full block pool, copied from `bridge_mobile_location`:
378/// `BridgeError` has no "Busy", `INVALID_PARAMETER` is the migration notes'
379/// designated stand-in, and the point is that the caller gets an explicit
380/// rejection instead of a promise that never settles.
377/// The answer for a full block pool. The call is valid, but all reply capacity
378/// is temporarily leased, so the page can retry rather than blaming its input.
381379fn poolFull() bridge_error.BridgeError {
382380 std.log.warn(
383381 "pickContact refused: all {d} async slots are in flight",
384382 .{ios_async.max_in_flight},
385383 );
386 return bridge_error.BridgeError.InvalidParameter;
384 return bridge_error.BridgeError.Busy;
387385}
388386
389387/// The answer for a second `pickContact` while one is on screen.
@@ -395,16 +393,15 @@ fn poolFull() bridge_error.BridgeError {
395393/// promise alive. Swift does neither: it overwrites `pendingCallbackId` and the
396394/// first caller waits forever on an untimed promise.
397395///
398/// Same `INVALID_PARAMETER` stand-in as `poolFull`, for the same reason —
399/// `BridgeError` has no word for "busy" — with a log line that says which of
400/// the two happened.
396/// Same retryable `Busy` answer as `poolFull`, with a log line that says which
397/// kind of capacity is occupied.
401398fn alreadyPresented() bridge_error.BridgeError {
402399 std.log.warn(
403400 "pickContact refused: a contact picker is already presented and still waiting for an " ++
404401 "answer; the first call is left alone rather than being stranded",
405402 .{},
406403 );
407 return bridge_error.BridgeError.InvalidParameter;
404 return bridge_error.BridgeError.Busy;
408405}
409406
410407// =============================================================================
@@ -1617,6 +1614,7 @@ test "a second request while one is presented is refused, and the first is left
16171614 // promise never settles; the picker on screen is about to answer, so the
16181615 // newcomer is the one that has to be told no.
16191616 try testing.expect(!publishPending(.{ .ticket = fakeTicket(4, 9), .sels = fakeSels() }));
1617 try testing.expectEqual(bridge_error.BridgeError.Busy, alreadyPresented());
16201618
16211619 // And the slot still holds the *first* ticket, not the newcomer's — a
16221620 // refusal that quietly replaced would look identical from the return value.
@@ -1626,6 +1624,10 @@ test "a second request while one is presented is refused, and the first is left
16261624 try testing.expectEqual(first.generation, pending.?.ticket.generation);
16271625}
16281626
1627test "a full async pool reports that the call is retryable" {
1628 try testing.expectEqual(bridge_error.BridgeError.Busy, poolFull());
1629}
1630
16291631test "the slot is handed over once, so a double-firing delegate cannot reply twice" {
16301632 resetPendingForTesting();
16311633 defer resetPendingForTesting();