ReviewOS

also looking at this

craft-native/craft

fix(ios): ask again when Core Location has no fix yet, rather than failing the caller

#264
Merged glennmichael123 wants to merge fix/ios-location-no-fix-yet into main
3 files +120 -7
9 days ago

Closes #260.

The flake was a real location bug

The five ids the slice job waited on aren't five hung calls. They're one chain of markers page.m sends once each answer arrives:

  1. getCurrentPosition (i=110) answers with coordinates, and the page sends marker 76.
  2. The cached request (i=111) answers, and it sends marker 78.
  3. The zero-timeout request (i=112) times out, and it sends marker 82.
  4. Speech ends, and it sends marker 72.
  5. openSettings answers, and it sends marker 54.

Both failing runs broke at the first link, in the same way:

info: craft-bridge dispatch t=mobile a=getCurrentPosition i=110
warning: location: CLLocationManager failed - domain=kCLErrorDomain code=0 …
FAIL: timed out after 900s waiting for: i=54 i=72 i=76 i=78 i=82

Those are jobs 106721185520 and 106411898966. A passing run (35626485544) shows 110, 111 and 112 in order, with no error.

Code 0 is kCLErrorLocationUnknown, which Apple documents as the manager being unable to get a location right away, not as a failure. Both the Zig bridge and the Swift shim rejected the waiting getCurrentPosition on any Core Location error. So on a loaded simulator, where the first fix is slow, request 110 failed at once, and the chain after it never started. A phone with a cold GPS would fail the same request the same way. The late Siri completion in the same logs was only a symptom of the slow simulator.

Change

Both sides use requestLocation, which gives up on that error rather than retrying on its own, so ignoring the error would just wait out the timeout. Instead, on kCLErrorLocationUnknown:

  • The one-shot asks again after a second, but only if the same caller is still the one waiting:

    • Zig checks the pending ticket's index and generation.
    • Swift checks singleLocationCallbackId.

    A caller that's been answered, timed out or replaced stops the asking, so the caller's own timeout still bounds the whole wait.

  • No craftLocationError event. A watch uses startUpdatingLocation, which does keep trying, so an error event would tell the page something that isn't true.

  • Every other error behaves exactly as before.

In Zig, the handling moved from didFailWithError into handleLocationFailure(domain, code, description), so it can be tested without an NSError. The retry uses ios_async.scheduleDeadline's dispatch_after, the same timer the timeout uses.

Tests, and what they don't prove

  • Zig: a new host test covers the full sequence.

    • A pending one-shot survives kCLErrorDomain/0.
    • Only the same ticket counts as still waiting.
    • A real failure (kCLErrorDomain/2) still settles the call.

    With the new branch disabled, it fails with error.NoFixYetSettledTheCall.

  • Swift: an index.test.ts assertion checks that the no-fix branch comes before anything that settles the caller or emits the event, and that the retry is guarded by the same callback id. typecheck-template.sh passes.

  • No end-to-end reproduction. I tried to reproduce it on the simulator harness by clearing the location and setting it seconds after the page asked. simctl location clear doesn't leave the simulator without a fix: it falls back to a default location, and the shim leg got San Francisco immediately. simctl has no way to put Core Location into "no fix yet", so I dropped that harness change rather than weaken the location case with it.

    The fix therefore rests on the diagnosis from the two CI logs and on the unit tests. If the slice job fails this way again, the log will now show location: no fix yet (kCLErrorLocationUnknown); asking again in 1000 ms lines, which would point past this cause.

3 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.