Skip to content

Hand metadata and group text to consumers as string_view - #260

Merged
kahrendt merged 2 commits into
mainfrom
view-api-metadata-group
Nov 11, 2026
Merged

kahrendt merged 2 commits into
mainfrom
view-api-metadata-group

Conversation

@kahrendt

@kahrendt kahrendt commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

First step of moving the public API from std::string to std::string_view. Metadata and group text already live in fixed storage (#256, #258); the public structs still copied it into std::strings kept allocation-free only by ReservedText (about 3 KB of internal RAM for metadata). The view forms hand consumers that storage directly; the string forms are deprecated and go in v0.11.0 with ReservedText. Part of Sendspin/backlog#92.

Migration: override on_track_metadata(const TrackMetadata&) instead of on_metadata(), on_group_state(const GroupState&) instead of on_group_update(), and call group_state() instead of get_group_state(). Text fields become std::optional<std::string_view>: copy them if you keep them past the callback (or, for group_state(), past the next return to loop() or stop()). A view is not guaranteed to be NUL-terminated, so print it with %.*s or copy it before handing it to a C-string API. docs/integration-guide.md "Deprecated String API" has the full table.

Change

  • Metadata: TrackMetadata views the role's current text block, valid for the callback.
  • Group: GroupState serves both the delta on_group_state() receives (the fields changed by the group/update messages since the last call) and the merged state group_state() returns. Both view an inline copy the main loop keeps, so the views outlive the string forms' removal.
  • Dispatch: the library calls the new callback, then the deprecated one; both default to no-ops, so a consumer overrides one. A stop() inside the new callback skips the deprecated call (group: the drain generation; metadata: TeardownTracker::completed(), since the protocol task may bump the cleanup generation meanwhile).
  • Deprecated with @deprecated and [[deprecated("...; removed in v0.11.0")]]: ServerMetadataStateObject, on_metadata(), GroupUpdateObject, on_group_update(), get_group_state(). A consumer of only the new API compiles warning-free; the library silences the warning only at its own compat sites.
  • get_track_progress_ms() / get_track_duration_ms() read the role's current state, so the removal can delete the deprecated object outright. The examples use the new API.

Tests

  • The metadata and group content tests check both forms carry identical content (cut text, unkept fields, cleared fields, delta versus merged state) and that the views do not point into the deprecated strings.
  • Listener-form tables: only new, only old, both (new first), a stop() from the new callback, and a protocol-task teardown during it (pins completed() over accepts()). The progress getters are pinned with and without a listener.
  • The metadata and group alloc windows stay at 0 with listeners overriding both forms. Mutants caught include views into the deprecated strings, the deprecated call skipped or reordered, the re-entrancy skip removed, and the getters reading the deprecated object.
  • Builds checked: -DENABLE_WERROR=ON with examples, -DSENDSPIN_ENABLE_METADATA=OFF, and an ESP-IDF esp32s3 build with no deprecation warnings from the library.

Next in the series: the pairing listener strings, server information and token getters; then the vector callbacks, the persistence provider and the configs.

TrackMetadata and on_track_metadata() deliver a metadata state with its
text as std::string_view into the role's current text block, and
GroupState, on_group_state() and group_state() deliver the group state
as views into an inline copy the main loop keeps. The library calls the
new callback first and then the deprecated one, skipping the deprecated
call after a re-entrant stop(), so existing consumers see no change.

ServerMetadataStateObject, on_metadata(), GroupUpdateObject,
on_group_update() and get_group_state() are deprecated and go in
v0.11.0, together with the ReservedText strings that back them. The
examples move to the new API, and the integration guide maps each old
name to its replacement.
@kahrendt
kahrendt marked this pull request as ready for review November 11, 2026 15:07
@kahrendt
kahrendt requested a max review from Copilot November 11, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Metadata and group-state callbacks can detach their listeners without stopping, causing subsequent delivery paths to dereference null listeners.

5 open issues
What changed in this PR

Begins the public text-API migration to std::string_view, exposing existing fixed storage for metadata and group text while retaining deprecated string-based APIs until v0.11.0.

Changes:

  • Adds TrackMetadata, GroupState, new callbacks, and the group_state() getter.
  • Delivers view callbacks before compatibility callbacks, with teardown guards.
  • Migrates examples and documentation, and extends content, lifecycle, and allocation tests.
File Description
tests/​test_encrypted_lifecycle.cpp Tests content parity, view storage, callback order, and progress getters.
tests/​test_client_lifecycle.cpp Migrates lifecycle tests and checks both group-state forms reset.
tests/​test_allocations.cpp Checks allocation behavior with both callback forms enabled.
tests/​lifecycle_test_fixtures.h Migrates the shared metadata listener.
tests/​CMakeLists.txt Documents updated white-box test access.
src/​teardown_tracker.h Exposes the completed teardown generation.
src/​protocol_messages.h Updates payload comments for view-backed public types.
src/​metadata_role.cpp Builds metadata views, delivers both forms, and updates progress reads.
src/​metadata_role_impl.h Declares view helpers and clarifies current versus compatibility state.
src/​client.cpp Maintains inline group state and dispatches both callback forms.
include/​sendspin/​types.h Adds GroupState and deprecates GroupUpdateObject.
include/​sendspin/​metadata_role.h Adds TrackMetadata and its callback; deprecates string forms.
include/​sendspin/​config.h Updates metadata configuration comments.
include/​sendspin/​client.h Adds the group view API and documents its lifetime.
examples/​tui_client/​tui.cpp Polls group state through the new getter.
examples/​tui_client/​main.cpp Migrates metadata and group callbacks.
examples/​basic_client/​main.cpp Prints metadata views using explicit lengths.
docs/​internals.md Describes view delivery, storage, and teardown handling.
docs/​integration-guide.md Documents migration, view lifetimes, and deprecated replacements.
docs/​conventions.md Establishes the convention for view-backed public text.

Review effort: Max

Comment thread src/client.cpp Resolved
Comment thread src/metadata_role.cpp Resolved
Comment thread docs/integration-guide.md Resolved
Comment thread docs/integration-guide.md Resolved
Comment thread tests/test_encrypted_lifecycle.cpp Resolved
Every set_listener() and provider setter now says to set it before
start() and leave it in place while the client runs, and the
integration guide's wiring step says replacing or clearing one while
running is not supported. The deprecated-API section describes the
view and string forms in the present tense and notes that a view is
not guaranteed to be NUL-terminated.
@kahrendt kahrendt added the enhancement New feature or request label Nov 11, 2026
@kahrendt
kahrendt enabled auto-merge (squash) November 11, 2026 17:30
@kahrendt
kahrendt merged commit 93ac6f8 into main Nov 11, 2026
36 of 48 checks passed
@kahrendt
kahrendt deleted the view-api-metadata-group branch November 11, 2026 17:36
kahrendt added a commit that referenced this pull request Nov 11, 2026
… copies (#261)

## Summary

Second step of moving the public API from `std::string` to
`std::string_view` (after #260). The pairing callbacks built a
`std::string` per call, and the pairing token and server information
getters built new strings on every call, all through `operator new`,
which aborts on ESPHome. The new forms hand over views or a fixed-size
array; the string forms are deprecated and go in v0.11.0. Part of
Sendspin/backlog#92.

**Migration:**

| Deprecated | Replacement |
|---|---|
| `on_pairing_started(const std::string&)` |
`on_pairing_attempt(std::string_view)` |
| `on_pairing_succeeded(const std::string&)` |
`on_paired(std::string_view)` |
| `on_pairing_failed(const std::string&, reason)` |
`on_pairing_aborted(std::string_view, reason)` |
| `on_display_pairing_code(const std::string&, format)` |
`on_pairing_code(std::string_view, format)` |
| `pairing_token()`, `format_pairing_token(psk)` |
`client_pairing_token()`, `pairing_token_for(psk)`:
`std::optional<std::array<char, PAIRING_TOKEN_LENGTH>>` |
| `get_server_information()` (`ServerInformationObject`) |
`server_information()` (`ServerInformation`), main loop only |

Callback views are valid for the callback; `server_information()`'s
until control next returns to `loop()` or `stop()`, like
`group_state()`. It can trail `is_connected()` by one `loop()`, so read
it on each `loop()` rather than only when `is_connected()` turns true.
Code off the main loop has no replacement for `get_server_information()`
and keeps calling it until v0.11.0. The token array carries no NUL.
Overriding a deprecated pairing callback draws no deprecation warning
(their parameter types are not deprecated), so search for the four old
names, and mark overrides `override` so their removal in v0.11.0 fails
to compile.

## Change

- **Dispatch:** each new pairing callback fires first, then its
deprecated form, which is skipped after a re-entrant `stop()`. The
deprecated forms fill a string reserved at construction rather than
building one per call, so they no longer allocate either.
- **Server information:** `ConnectionManager` publishes the primary's
server through a new Inbox slot only when the primary changes (about 115
bytes then, nothing per `loop()`); `drain_inbox()` takes it before any
callback, and `stop()` clears the copy.
- **Tokens:** `PAIRING_TOKEN_LENGTH` (107) is a public constant, checked
against the internal encoder's size.
- A scoped pragma in `client.h` wraps the deprecated
`get_server_information()` declaration: GCC otherwise warned in every
file including the header, because the declaration names the deprecated
type.
- The examples and the integration guide (pairing sections, Querying
State, the Deprecated String API table) use the new API.

## Tests

- `ClientLifecycle.PairingCallbacksReachBothForms`: only new, only
deprecated, both (new first) and a `stop()` from the new form, across
every abort reason and both code formats; the pairing state-machine
suite checks every text callback is followed by its deprecated form with
the same content.
- Token getters: `nullopt` before `start()`, equal to the encoder over
the client's identity (configured and stored PSK) and to the deprecated
getters.
- `server_information()` matches the deprecated getter across restart,
re-handshake, pairing, outbound, a liveness drop and a two-server
handoff, and is cleared by `stop()`.
- Alloc windows: the `pairing_psk` and code-pairing windows (digits and
QR) drop from 2, 2 and 3 `operator new` calls to 0 with listeners
overriding both forms; admission and the playback handoff cover the
server-information refresh at 0. Mutants caught include a skipped or
reordered deprecated call, the `stop()` skip removed, views into the
deprecated string, an unreserved compat string and a copy never
refreshed or cleared.
- Builds checked: `-DENABLE_WERROR=ON` with examples, and an ESP-IDF
esp32s3 build with no deprecation warnings. Protocol task stack bound
unchanged (6,512 B at -Os).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants