Skip to content

feat(dialog): batch table deltas — append/update/remove without resending rows#152

Merged
facontidavide merged 2 commits into
mainfrom
feat/dialog-table-delta-ops
Jul 18, 2026
Merged

feat(dialog): batch table deltas — append/update/remove without resending rows#152
facontidavide merged 2 commits into
mainfrom
feat/dialog-table-delta-ops

Conversation

@facontidavide

Copy link
Copy Markdown
Contributor

Summary

A 10,000×6 table costs ~25 ms and ~1 MB of JSON per full-state refresh (measured; grows ~linearly), so live feeds (streaming fault lists, status boards) had no efficient way to change one row. This adds batch table deltas to the dialog protocol — JSON-level, backward compatible, PJ_DIALOG_PROTOCOL_VERSION unchanged:

  • WidgetData::appendTableRows / updateTableCells / removeTableRows → per-widget table_delta {seq, append, update_cells, remove_rows} (+ TableCellUpdate).
  • seq contract (reworked after Codex review): plugin-owned, fresh value per delta; hosts apply only when seq differs from the last applied to that widget — restart-safe and immune to the UINT64_MAX "must increase" dead-end; a full-state rebuild carrying an old delta is harmless on all three delivery paths (key-diffing, byte-identical drop, diffing disabled). Ops of one refresh share one seq — tableDeltaEntry discards prior ops on a differing seq, so stale ops can never be relabeled (tested). rows in the same refresh wins and consumes the delta.
  • Strict decoding: one malformed op (wrong type/arity, negative index) rejects the whole delta — partial application would consume the seq while leaving the table diverged with no repair path. remove_rows arrives normalized (descending, duplicate-free) so hosts can apply in vector order with no index-shift hazard. tableDeltaSeq() lets hosts check staleness before paying for the full decode.
  • dialog-plugin-guide.md gains a "Large tables" section: every widget-data field is optional (omit-unchanged is the primary pattern), the efficiency ladder, and the delta contract. Marked SDK-side only: hosts apply table_delta from the companion PlotJuggler change onward (queued next); older hosts ignore the key by design.

Versioning

Rides 0.18.0 together with #150 (QDateTimeEdit events) — no version-file changes here to avoid a bump collision; CHANGELOG entries merge under the one [0.18.0] heading on whichever PR lands second. Header-only additions, abidiff additions-only.

Review trail (pipeline: codex → simplify → PR)

  • Codex review returned needs rework (4 major / 2 minor) on the original at-most-once seq contract, lenient decoding, duplicate removals, per-op seq relabeling, and doc ambiguities — all addressed above, each with a test where testable (mixed-seq reset, malformed-op rejection, negative-index rejection, removal normalization; watched fail first).
  • Cleanup pass: reuse/simplification clean (view-side hand-rolled parsing is the file's documented idiom); efficiency finding (full decode before staleness check) → tableDeltaSeq(); altitude findings → unsourced-precision numbers softened, SDK-only status made explicit.

Test plan

Round-trip + absent/malformed/normalization tests in widget_data_test.cpp / widget_data_view_test.cpp; full SDK suite 49/49; pre-commit clean.

🤖 Generated with Claude Code

facontidavide and others added 2 commits July 18, 2026 10:26
…ding rows

Large tables (10k+ rows) cost ~25 ms per full-state refresh (~1 MB JSON
round-trip); a live feed appending one row per event had no cheaper option
than resending the whole array. Add batch deltas (JSON-level, backward
compatible; no C ABI change):

- WidgetData::appendTableRows / updateTableCells / removeTableRows write a
  per-widget table_delta {seq, append, update_cells, remove_rows}. seq is
  plugin-owned; hosts apply a delta only when its seq differs from the last
  applied (restart-safe, no wraparound edge), as update_cells -> remove_rows
  -> append, all indexes addressing the pre-delta table. Ops of one refresh
  share one seq — a differing seq starts a fresh delta, so stale ops can
  never be relabeled. rows in the same refresh wins and consumes the delta.
- WidgetDataView::tableDelta() decodes strictly (a malformed op rejects the
  whole delta — partial application would consume the seq while diverging
  from the plugin's model) and normalizes remove_rows to descending unique;
  tableDeltaSeq() is the cheap staleness pre-check.
- dialog-plugin-guide.md gains a "Large tables" section: the
  omit-unchanged-fields pattern (every widget-data field is optional), the
  efficiency ladder, and the delta contract. SDK-side only: hosts apply
  table_delta from the companion PlotJuggler change onward; older hosts
  ignore the key (harmless no-op).

Rides the 0.18.0 release together with the QDateTimeEdit event PR (#150);
CHANGELOG entries merge under one heading on whichever lands second.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@facontidavide
facontidavide merged commit f061e33 into main Jul 18, 2026
3 of 4 checks passed
@facontidavide
facontidavide deleted the feat/dialog-table-delta-ops branch July 18, 2026 12:27
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