Skip to content

fix(bindings): close the three open issues (#368, #471, #472) - #502

Merged
bahdotsh merged 3 commits into
mainfrom
fix/open-issues-368-471-472
Oct 2, 2026
Merged

bahdotsh merged 3 commits into
mainfrom
fix/open-issues-368-471-472

Conversation

@bahdotsh

@bahdotsh bahdotsh commented Oct 2, 2026

Copy link
Copy Markdown
Member

Closes #368, closes #471, closes #472. One commit per issue, each independently revertable.

#368: Python relay-answer prefixes

0.27.0 (#453) already corrected the prefix names and attribution at each call site. What was left is the issue's third item: Python held no copy of the relay-answer list and no pin.

  • relay_answer_prefixes.py holds the six prefixes. _inject_group_frame passes every actor through attributable_actor, so an answer reaches the core with no transport identity whatever the call site passes. This matches the central rule the Swift and Kotlin bridges already have.
  • test_relay_answer_prefixes.py pins the six literals. A new injection test proves an attributed answer is still stripped.
  • Every doc that counted three copies now counts four: prefixes.rs, the control-messages chapter, ADR 0004, C5, K7, S7, and the bridge headers. New contract P12 in docs/bridges/python.md.

Deployed apps: no behaviour change for a correct caller.

#471: DiagnosticEvent.level

  • The union is now 'debug' | 'info' | 'warning' | 'error' in types.ts and the RN README. Both platforms emit debug, at about 70 Swift and Kotlin sites.
  • The scan also found that Android's PeripheralGattServer emitted four diagnostics as warn, a spelling nothing declared. They are warning now.
  • react_native_diagnostic_levels_match_every_bridge reads the union and every level literal handed to a diagnostic sink in Swift, Kotlin and Python. It fails in both directions. It has floors on files and literals, so a scan that stops matching cannot pass on nothing.

Deployed apps: the type now matches what already arrives at runtime, but this is not automatically source-compatible. An exhaustive switch or a Record keyed by the level needs a debug entry. A filter comparing against 'warn' for the four GATT diagnostics should compare against 'warning'.

#472: setInterest before start() on React Native

The engine's start-up exchange offers every held space with the interest in force at that moment, and narrowing never deletes what a wider exchange pulled in. React Native's start() opens storage and starts the engine in one call. So the documented "before start()" was rejected natively, and "after start()" let the first exchange ask for everything.

  • DataStore.setInterest holds a declaration made before start(). start() applies it after MLS initialization and before the native start. After that, calls go straight to native.
  • A refused pattern rejects start() with an error naming the space and carrying the native code. The engine is not started, because starting would replicate the space whole.
  • When the data layer is off, the held call is dropped with a warning and start() proceeds. This is checked with a probe, because the engine validates the space name before it checks the layer.
  • destroy() discards the hold. It does so only for an instance that created an engine, since the state is module-wide.
  • Both native destroy() paths now release the data store. It holds a strong reference to the engine, so data calls after destroy() reached the stopped engine instead of rejecting, and on Android the engine stayed alive until garbage collection.
  • Docs updated: data.md, the API reference, the RN integration guide, UPGRADING section 23, and a C7 row. A note in docs/bridges/python.md says that ProtocolManager still runs its first exchange under the default interest. That follow-up is not fixed here.

Deployed apps: an invalid pattern declared before start() used to reject that call and now rejects start(). A call after start() is unchanged. An app that worked around 0.27.0 by calling after start() can move the call back.

Validation

  • cargo clippy --workspace -- -D warnings, cargo test --workspace --lib, RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps, cargo fmt --all -- --check: all clean.
  • Python: the full suite passes (808 passed, 4 skipped).
  • npm run test:js: all 11 harness files pass. The new data-interest-order.test.js has 12 cases.
  • iOS: the CI swiftc -typecheck step, which includes OfflineProtocolModule.swift, passes locally. A negative control proved the run was not vacuous.
  • Android: the module compiles and 556 unit tests pass in a clean copy. The 10 PeerStreamFramingTest failures there come from the copy living outside the repository, where the test cannot find its vector file.
  • Mutation-tested: each literal pin, the central attribution rule, the level guard in three directions, the start ordering (JS harness and Rust guard), the destroy reset and its scoping, the array copy, and the layer-off branch and probe. Every mutant fails a test.
  • Not run: no device or simulator run of the start ordering or the native destroy change.

…ution centrally

Python held no copy of the relay-answer exemption list, so the rule that a
relay answer reaches the core with no transport identity was enforced at
each call site rather than once. relay_answer_prefixes.py now holds the list,
_inject_group_frame forces every answer unattributed, and a literal pin fails
the Python suite the next time the registry moves. The docs that counted
three copies now count four.

Closes #368
…ads every emitter

DiagnosticEvent.level omitted debug, which both native platforms emit, and
Android's GATT server emitted a fifth spelling, warn, that nothing declared.
The union now names the four levels the bridges emit, the four warn sites are
warning, and react_native_diagnostic_levels_match_every_bridge reads the
union and every level literal handed to a diagnostic sink in Swift, Kotlin
and Python, failing in both directions.

Closes #471
… apply it before the engine starts

The engine's start-up exchange offers every held space with the interest in
force at that moment, and a narrowing never deletes what a wider exchange
pulled in. React Native's start() opens storage and starts the engine in one
call, and the setInterest JSDoc said to call it before start(), where both
native modules rejected it. DataStore.setInterest now holds a declaration
made before start(), and start() applies it after MLS initialization and
before the native start; a refused pattern rejects start() without starting
the engine, and a data layer that is off drops it with a warning.

Both native destroy() paths now release the data store, which holds a strong
reference to the engine and otherwise answered data calls after destroy().

Closes #472
@bahdotsh
bahdotsh merged commit 0dfae93 into main Oct 2, 2026
24 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

1 participant