Skip to content

feat(shadows): decompose nested report_only(persist) fields to KV - #135

Merged
MathiasKoch merged 1 commit into
masterfrom
feat/report-only-persist-decompose-nested
Jul 7, 2026
Merged

MathiasKoch merged 1 commit into
masterfrom
feat/report-only-persist-decompose-nested

Conversation

@MathiasKoch

@MathiasKoch MathiasKoch commented Jul 6, 2026 •

Copy link
Copy Markdown
Member

Summary

report_only(persist) persisted each field as one whole-value postcard blob via a separate ReportedFields::persist_report_only path. That couldn't round-trip a nested #[shadow_node] (struct or adjacently-tagged enum) against the decomposed load_from_kv, and demanded MaxSize on the whole type — so a nested persisted enum didn't even compile.

This persists the Reported side decomposed, mirroring the State persist_to_kv key layout so load_from_kv reads it back. Since a Reported is a partial projection, unreported (None) fields keep their prior KV value — a partial report merges with prior state at load, with no read-modify-write. The shape parallels persist_delta (the same skip-None decomposed walk over Delta).

⚠️ Migration

The KV layout for a non-opaque report_only(persist) field changes. A nested #[shadow_node] value now persists as decomposed keys (/field/...) instead of a single whole-value blob at /field. A device that previously persisted such a field as a blob will not find the decomposed keys on upgrade, so the value resets to its default on first boot after the update.

To keep the previous blob layout (and avoid the reset), add opaque to every report_only(persist) attribute you want unchanged:

// now decomposes (new default)
#[shadow_attr(report_only(persist))]

// keeps the old whole-value blob layout
#[shadow_attr(report_only(persist), opaque(max_size = N))]

Notes:

  • Leaf and scalar fields already persisted as a single key and are unaffected either way.
  • Adjacently-tagged enums can't be postcard-blobbed, so they must use the new decomposed form (they didn't compile as report_only(persist) before this PR).

Coverage

  • Terminal leaf types whose Reported == Self — primitives, std String/Vec, heapless::String — delegate to their existing persist_to_kv.
  • The default persist_reported_kv errors with KvError::Unsupported (not a silent drop) for types that can't be decomposed without narrowing ShadowNode (heapless::Vec/LinearMap/arrays would force T: MaxSize onto their ShadowNode impl), need a per-entry walker (maps), or are non-adjacently-tagged enums. Mark such a field opaque to store it as a whole-value blob instead.

Notes

`report_only(persist)` previously persisted each field as one whole-value
postcard blob via a separate `ReportedFields::persist_report_only` path. That
couldn't round-trip a nested `#[shadow_node]` (struct or adjacently-tagged enum)
against the decomposed `load_from_kv`, and demanded `MaxSize` on the whole type
— so a nested persisted enum simply didn't compile.

Persist the `Reported` side decomposed instead, mirroring the State
`persist_to_kv` key layout so `load_from_kv` reads it back. Because a `Reported`
is a partial projection, unreported (`None`) fields keep their prior KV value:
a partial report merges with prior state at load, with no read-modify-write.

Terminal leaf types whose `Reported == Self` (primitives, std `String`/`Vec`,
`heapless::String`) delegate to their existing `persist_to_kv`. The default
`persist_reported_kv` errors with `KvError::Unsupported` rather than silently
dropping data, for types that can't be decomposed without narrowing `ShadowNode`
(`heapless::Vec`/`LinearMap`/arrays would force `T: MaxSize`) or need a per-entry
walker (maps) / a non-adjacently-tagged enum — mark such a field `opaque`.
@MathiasKoch
MathiasKoch merged commit deb6d79 into master Jul 7, 2026
5 checks passed
@MathiasKoch
MathiasKoch deleted the feat/report-only-persist-decompose-nested branch July 7, 2026 08:29
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.

2 participants