Skip to content

Bump to 0.7.31 — frame-tracker ownership fix + honest platform docs - #90

Merged
GenericJam merged 3 commits into
masterfrom
fix/mob-103-followups
Aug 27, 2026
Merged

Bump to 0.7.31 — frame-tracker ownership fix + honest platform docs#90
GenericJam merged 3 commits into
masterfrom
fix/mob-103-followups

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

First run of the release gate that landed in 0.7.30. Reviewing 0.7.29..master caught three things in the MOB-103 work that 0.7.30 already shipped.

1. iOS frame tracker lost its ownership token on a refused write (real bug)

record assigned box.seq unconditionally, so a generation-refused write (returns 0) overwrote a valid seq with 0 — and mob_unregister_frame ignores seq 0, permanently disabling that tracker's own cleanup.

Failure: push from a screen tagging :id "save" to one whose "save" is a lazy row below the fold. The outgoing screen's .move writes are refused by the generation gate, box.seq becomes 0, its .onDisappear is a no-op, the id stays in the tree so the purge keeps it — and Mob.Test.tap_id taps the old screen's coordinates.

Keeping the last successful seq is strictly safe: the compare-and-delete still refuses to delete whenever an incoming tracker has claimed the id since. The .opacity (reset) transition doesn't hit this, which is the tell — it doesn't move the outgoing frame, so nothing gets refused.

2. element_frames/1 docs implied cross-platform parity that doesn't exist

The drop-when-not-laid-out behaviour is iOS only. Android clears frames wholesale on a navigation transition and never per element (MobBridge.setRootJson), so a scrolled-away lazy_list row keeps its frame and tap_id happily taps it. Verified in the template source. Docs now say so plainly instead of implying parity; Android parity wants its own issue.

3. 0.7.30 published with empty release notes

CHANGELOG's top section was still ## [Unreleased] when 0.7.30 was tagged, so release.yml's ## [<version>] extraction found nothing. Retitled to [0.7.30] so the notes exist for anyone reading back. Exactly the version/release-seam class the gate exists to catch.

Also

Wires make -C test/native run into the macOS CI job. That harness is the only automated guard on the registry's ownership/generation/purge rules, and it reproduces the algorithm rather than linking it — running it is what keeps the copy honest. It was committed in 0.7.30 but nothing ran it.

Preflight

mix format --check-formatted, mix credo --strict, mix compile --warnings-as-errors, 1111 tests, clang-format, and the native harness (20/20) all clean.

🤖 Generated with Claude Code

GenericJam and others added 3 commits August 27, 2026 03:24
Release-gate review of 0.7.29..master (the policy that landed in 0.7.30)
caught three things in the MOB-103 work that 0.7.30 already shipped.

1. iOS: a frame tracker whose write was REFUSED lost its ownership token.
   `record` assigned box.seq unconditionally, so a generation-refused write
   (returns 0) overwrote a valid seq with 0 — and mob_unregister_frame ignores
   seq 0, permanently disabling that tracker's own cleanup. It bites when an
   outgoing screen's .move writes are refused by the generation gate AND the
   incoming screen's element with the same :id isn't laid out (a lazy row below
   the fold): the id stays in the tree so the purge keeps it, nothing deletes
   it, and Mob.Test.tap_id taps the old screen's coordinates. Keeping the last
   SUCCESSFUL seq is strictly safe — the compare-and-delete still refuses to
   delete whenever an incoming tracker has since claimed the id. The .opacity
   (reset) transition doesn't hit this, which is the tell: it doesn't move the
   outgoing frame, so nothing gets refused.

2. Mob.Test.element_frames/1 documented the drop-when-not-laid-out behaviour as
   if it were cross-platform. It is iOS-only: Android clears frames wholesale on
   a navigation transition and never per element (MobBridge.setRootJson), so a
   scrolled-away lazy_list row keeps its frame and tap_id happily taps it. The
   docs now say so plainly rather than implying parity. Android parity is worth
   its own issue.

3. CHANGELOG's top section was still [Unreleased] when 0.7.30 was tagged, so
   release.yml's `## [<version>]` extraction found nothing and published 0.7.30
   with empty release notes. Retitled to [0.7.30] so the notes exist for anyone
   reading back, with this release's entries above it.

Also wires `make -C test/native run` into the macOS CI job. That harness is the
only automated guard on the registry's ownership/generation/purge rules and it
reproduces the algorithm rather than linking it, so running it is what keeps the
copy honest — it was committed in 0.7.30 but nothing ran it.

Preflight: format, credo --strict, compile --warnings-as-errors, 1111 tests,
clang-format, and the native harness (20/20) all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Android had no dismiss sender at all, so mob_new's generated sheet renderer
fell back to nativeSendTap and delivered {:tap, tag}. Mob.UI.sheet/2 documents
:on_dismiss as {:dismiss, tag} (lib/mob/ui.ex:287) and iOS has always sent that
(mob_send_dismiss, ios/mob_nif.m:233), so a screen written to the contract never
matched: Mob.Screen forwards the unmatched message to handle_info, which raises
FunctionClauseError and takes the screen down — or silently drops it with a
catch-all, in which case the BEAM never learns the sheet closed and can't
re-present it.

sendEvent already parameterises the atom, so this is the tap sender with
"dismiss" instead of "tap", plus the mob_beam.h declaration the generated
beam_jni.c thunk compiles against.

Paired with mob_new's MOB-104 change, which adds the nativeSendDismiss extern
and JNI thunk and switches sendDismissOnce over. The coupling is compile-
enforced: mob_new's "beam_jni.c passes clang -fsyntax-only" test builds the
rendered thunk against MOB_DIR's mob_beam.h, so mob_new CI fails until this
lands. mob releases first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oss-refs

- CHANGELOG: 0.7.31 never mentioned mob_send_dismiss, the new exported symbol
  mob_new's generated beam_jni.c links against. Someone hitting the
  "call to undeclared function 'mob_send_dismiss'" build error would have
  searched the changelog and found nothing. Added, with the Android behaviour
  change spelled out: anyone who worked around the old bug by matching
  {:tap, tag} for a sheet dismissal has a dead clause now. Also reinstated the
  [Unreleased] heading that the previous commit deleted outright, so the next
  change has somewhere to land.

- test/native: the harness narrated the PRE-fix caller ("refused write leaves
  seq 0, so disappear can't delete") while asserting behaviour the fixed caller
  no longer produces — it passed by a different mechanism than the comment
  claimed, and the scenario the fix actually exists for had no coverage at all.
  Reverting MobRootView.swift left CI green. Case 4 now describes the retained-
  token model, and 4b covers the untested case: outgoing writes refused AND no
  incoming tracker ever claims the id, so the retained token is the only thing
  that can clean up. Both caller behaviours are modelled side by side, since the
  difference lives in Swift and this harness only has the C half.

- lib/mob/test.ex: the Android caveat said parity was "tracked separately"
  without naming an issue, unlike every other cross-reference here. Filed and
  cited MOB-105.

- CHANGELOG: fixed the dangling "note the Unreleased entry above" in 0.7.29,
  left pointing at a heading the previous commit renamed.

Preflight: format, 1111 tests, clang-format, native harness all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GenericJam

Copy link
Copy Markdown
Owner Author

mob_send_dismiss device-verified (physical Moto G power, 2021)

Verified through the full JNI chain rather than just statically:

onDismissRequestMobBridge.nativeSendDismissJava_com_example_sheetprobe_MobBridge_nativeSendDismissmob_send_dismisssendEvent(handle, "dismiss"){:dismiss, tag} delivered to the screen's handle_info/2.

The probe screen matched both {:dismiss, tag} and {:tap, tag} so a wrong shape would be logged rather than kill the screen. Result across two open/dismiss cycles: log: [:dismiss, :dismiss], the :tap clause never fired, screen alive throughout, sheet re-presented correctly after each dismissal.

Paired with GenericJam/mob_new#38, which supplies the nativeSendDismiss extern and JNI thunk.

Release ordering: mob_new's CI clones GenericJam/mob at master with no ref pinned, so merging this PR unblocks mob_new's CI immediately — it currently fails with call to undeclared function 'mob_send_dismiss', which is the lockstep working as intended.

@GenericJam
GenericJam merged commit 2bd443d into master Aug 27, 2026
4 checks passed
@GenericJam
GenericJam deleted the fix/mob-103-followups branch August 27, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant