Skip to content

Freeze event-type ABI: internal constructors + consistent copy() (closes #15)#26

Merged
mibrahimdev merged 1 commit into
mainfrom
feat/15-freeze-event-abi
Jun 14, 2026
Merged

Freeze event-type ABI: internal constructors + consistent copy() (closes #15)#26
mibrahimdev merged 1 commit into
mainfrom
feat/15-freeze-event-abi

Conversation

@mibrahimdev

Copy link
Copy Markdown
Owner

What

HttpEvent, MqttEvent, BleEvent were public data classes with all-public constructors, but instances are only ever built inside the loggers — consumers only read them. Public constructors (and copy()) freeze the field list: adding a field later is a source/binary break. This narrows the surface so a field can be added later without breaking consumers. Closes #15.

Changes

  • Primary constructors → internal on all three event types in both :sharingan and :sharingan-noop. data is kept, so consumers can still destructure (componentN) and when-match.
  • Added @ConsistentCopyVisibility so the generated copy() / copy$default follow the now-internal constructor.
  • Regenerated BCV dumps (.api + .klib.api) for both modules.

⚠️ Decision for your review: copy() is also frozen

The issue asked for internal constructors. An internal constructor alone leaves copy() public (Kotlin keeps it public for back-compat) — so event.copy() would still be a frozen ABI and adding a field would still break it. That's a half-freeze that doesn't deliver #15's stated goal.

I went the full-freeze route with @ConsistentCopyVisibility, which removes public copy()/copy$default too. Net effect on the public surface:

- public fun <init> (…)              ← removed
- public synthetic fun <init> (…)    ← removed
- public final fun copy (…)          ← removed
- public static synthetic fun copy$default (…)  ← removed
  public final fun component1..N ()  ← kept (destructuring)
  public final fun getX ()           ← kept (reading)
  equals / hashCode / toString       ← kept

If you'd rather keep copy() public (constructor-only freeze), drop the three @ConsistentCopyVisibility annotations and re-run ./gradlew apiDump.

Parity note (relevant to #12)

The change is symmetric across both modules, so the frozen surface stays identical between :sharingan and :sharingan-noop. Separately, I noticed a pre-existing divergence the parity check (#12) must handle: :sharingan event classes carry a Compose $stable field that :sharingan-noop doesn't (Compose is only on the real module). It predates this PR — flagging it so #12's parity check normalizes/ignores $stable rather than failing on it.

Acceptance criteria

  • Event-type primary constructors are internal
  • Consumer read/when-match/destructure still compiles (sample builds; sample constructs no events)
  • BCV .api dumps regenerated to reflect the narrowed surface
  • No asymmetric change to the noop surface (parity preserved)

Verification

:sharingan:testDebugUnitTest + :sharingan-noop:testDebugUnitTest green · ./gradlew apiDump regenerated · :sample:composeApp:compileDebugKotlinAndroid green.

Note: the full build+iOS-test gate (#22, PR #25) isn't on main yet, so this PR only runs api-check. Once #25 merges, rebasing this PR would also exercise the iOS build.

🤖 Generated with Claude Code

After this commit HttpEvent, MqttEvent and BleEvent expose no public way to
construct or copy() them — only to read, destructure, and `when`-match. This
unfreezes their field list: a future field can be added without a source or
binary break for consumers, the whole point of #15.

What changed:
- Primary constructors are now `internal` (instances are only built inside the
  loggers/PreviewData, all in-module). `data` is kept, so componentN
  destructuring and `when`-matching still work for consumers.
- Added `@ConsistentCopyVisibility` so the generated `copy()`/`copy$default`
  inherit the internal constructor's visibility. Without it Kotlin keeps copy()
  public for back-compat, leaving the copy() ABI frozen against new fields —
  a half-freeze that defeats the issue's purpose.
- Regenerated the BCV dumps (jvm `.api` + native `.klib.api`) for both modules:
  the `<init>`, `copy` and `copy$default` entries drop out; getters, componentN,
  equals/hashCode/toString and the computed props stay.

Parity: the change is symmetric across :sharingan and :sharingan-noop, so the
frozen surface stays identical between them. (The pre-existing `$stable`-field
divergence — Compose-only on :sharingan — is untouched here and is for #12 to
reconcile.)

Verified: :sharingan + :sharingan-noop testDebugUnitTest green; apiDump
regenerated; sample (consumer) compiles against the narrowed surface; sample
constructs no events directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mibrahimdev mibrahimdev force-pushed the feat/15-freeze-event-abi branch from 8b38f1c to 8a96f9c Compare June 14, 2026 13:03
@mibrahimdev mibrahimdev merged commit a58147e into main Jun 14, 2026
3 checks passed
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.

Freeze event-type ABI: make HttpEvent/MqttEvent/BleEvent constructors internal

1 participant