Detect changes by value, not by the controller's lc (#295) - #575
Merged
Merged
Conversation
"Record changes only" decided whether a value had changed from `state.ts !== state.lc`. That is right for an ordinary state - js-controller only moves `lc` when the value really changes - but wrong for an alias. An alias has no value of its own, so it carries the `lc` of its source: a source going from 12.34 to 12.31 moves `lc`, and the alias arrives with ts === lc although its read converter maps both to 12.3. Every source change was stored, which is what the reporter saw as hundreds of identical rows. The comparison now runs against `sqlDPs[id].state.val`, which is assigned only on the logging path and therefore holds the value that was last written - exactly the reference "changes only" needs. The neighbouring changesMinDelta check already compared values this way. isSameValue() lives in src/lib/values.ts so it can be unit tested without importing main.ts, and so that the two cases that are easy to get wrong are in one place: objects are compared by their JSON, because ts_string stores them JSON.stringify()ed and a strict !== would call every update a change and defeat changesOnly for those datapoints; two NaN readings count as unchanged. Three of the four occurrences are replaced - the changesOnly branch with and without a relog interval, and the relog trigger. The fourth, in the debounce branch, is deliberately left alone: there the right reference is the value the running timer is about to store, not the last stored one, and the adapter does not track it separately. Comparing against the stored value would make a 5 -> 7 -> 5 excursion inside the debounce window look unchanged and keep the timer from restarting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #295. "Record changes only" stored a row for every source change of an alias, even when the read converter mapped them all to the same value — the reporter saw hundreds of identical rows, and two other users confirmed it.
Cause
pushHistory()decided whether a value had changed fromstate.ts !== state.lc:That is right for an ordinary state — js-controller only moves
lcwhen the value really changes. It is wrong for an alias: an alias has no value of its own, so it carries thelcof its source. A source going from12.34to12.31moveslc, and the alias arrives withts === lcalthoughMath.round(val*10)/10maps both to12.3.This is what @Apollon77 described in 2024 — "fixing this requires that the adapter builds up his own last value cache and do the comparison based on the real value". That cache already exists:
sqlDPs[id].stateis assigned only on the logging path, right beforepushHelper(), so it holds the value that was last written — exactly the referencechangesOnlyneeds. The neighbouringchangesMinDeltacheck already compared values this way.Scope
Three of the four occurrences are replaced: the
changesOnlybranch with and without a relog interval, and the relog trigger.The fourth, in the debounce branch, is deliberately left alone. There the correct reference is the value the running timer is about to store, not the last stored one — and the adapter does not track it separately. Comparing against the stored value would make a
5 → 7 → 5excursion inside the debounce window look unchanged and keep the timer from restarting, which would be a new bug rather than a fix.Two cases that are easy to get wrong
isSameValue()lives insrc/lib/values.tsso it is unit testable without importingmain.ts, and so both land in one place:ts_stringstores themJSON.stringifyed. A strict!==compares references, would call every update a change, and would defeatchangesOnlyfor exactly those datapoints — the opposite of this fix.NaNreadings count as unchanged, sinceNaN === NaNis false but a sensor repeating NaN has not changed.A string and a number that look alike (
'12.3'vs12.3) stay a change: they go into different tables, so collapsing them would lose a real transition.Verification
test/testValues.js, 8 cases, in the CI unit list — including the issue's own converter. 192 unit tests pass,npm run check:ts,npm run lint(both passes) andprettier --checkare clean.Because this touches the write path, it was also run against the full SQLite integration suite locally: 36 passing, 0 failing. The changed branch is genuinely exercised there —
value not changedappears 6× in the debug log (thechangesOnlybranch skipping correctly) andvalue not changed debounce2× (the branch left untouched).🤖 Generated with Claude Code