Skip to content

fix(store): replace Object.is with structural equality in CreateHistr…#1762

Open
palak170306-design wants to merge 1 commit into
Karanjot786:mainfrom
palak170306-design:fix/history-store-object-is-equality
Open

fix(store): replace Object.is with structural equality in CreateHistr…#1762
palak170306-design wants to merge 1 commit into
Karanjot786:mainfrom
palak170306-design:fix/history-store-object-is-equality

Conversation

@palak170306-design

@palak170306-design palak170306-design commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Description

createHistoryStore.set() was using Object.is for deduplication instead of the JSON.stringify structural equality promised by the JSDoc comment. This caused duplicate history entries to be pushed for object and array states that were deeply equal but different references. This PR fixes the equality check and adds an optional equals comparator to HistoryStoreOptions for non-serialisable types.

Related Issue

Closes #1742

Which package(s)?

@termuijs/store

Type of Change

🐛 Bug fix (type:bug)
🧪 Tests (type:testing)

Checklist

⭐ You starred the repo. The needs-star check blocks your merge otherwise.
Tests pass locally: bun vitest run
Build passes: bun run build
Typecheck passes: bun run typecheck
You read CONTRIBUTING.md.
Your PR title follows type: short description.
No new any types without an inline comment explaining why.
No unrelated refactors bundled into this PR.

GSSoC 2026 Participation

You are a GSSoC 2026 contributor.
Your GSSoC profile: https://gssoc.girlscript.org/profile/048e6a97-baaf-4a03-b6e5-bdfc6c38e173

Screenshots / Recordings (UI changes)

No UI changes — pure logic fix in @termuijs/store.
Notes for the Reviewer
Root cause: set() on line 61 of history.ts used Object.is(timeline.present, newState) for its dedup guard. Object.is is reference equality — for primitive types like string this coincidentally works, which is why all existing tests passed. For object or array state, two deeply-equal values with different references always fail the check, causing a duplicate past[] entry on every set() call.

What changed:

Added HistoryStoreOptions interface with an optional equals?: (a: T, b: T) => boolean field
Added options: HistoryStoreOptions = {} parameter to createHistoryStore
Defined a const equals inside the function body that defaults to JSON.stringify structural comparison with an Object.is fallback inside a try/catch (guards against circular references or BigInt which cause JSON.stringify to throw)
Replaced Object.is(timeline.present, newState) with equals(timeline.present, newState) — the only change to existing logic

New tests added (all in history.test.ts):

Object state with same content does not push a duplicate entry
Object state with changed content does push an entry
Array state with same content does not push a duplicate entry
Custom equals function works correctly for non-serialisable types (Map)

All 8 existing tests pass unchanged since they use string state, where JSON.stringify equality is identical in behaviour to Object.is.
No breaking changes — options defaults to {} so all existing createHistoryStore(initial) call sites work without modification.

Summary by CodeRabbit

  • Bug Fixes

    • Improved state deduplication to prevent unnecessary history entries when states are structurally equivalent, even with different references.
  • New Features

    • Added custom equality comparison option for history store to handle complex and non-serializable data types.
  • Tests

    • Enhanced coverage for state deduplication scenarios.

@github-actions github-actions Bot added type:bug +10 pts. Bug fix. type:testing +10 pts. Tests. labels Jun 22, 2026
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

createHistoryStore gains an optional HistoryStoreOptions<T> parameter with an equals callback. The default equality check changes from Object.is (reference equality) to JSON.stringify-based structural equality with an Object.is fallback when stringification throws. Tests are added for object, array, and Map deduplication scenarios.

Changes

Structural equality deduplication in createHistoryStore

Layer / File(s) Summary
HistoryStoreOptions interface and equals logic
packages/store/src/history.ts
Exports new HistoryStoreOptions<T> interface with optional equals callback. Updates createHistoryStore signature to accept it as a second argument. Adds a default equals function using JSON.stringify with Object.is fallback on stringify errors. Updates the set() guard to call the configured comparator instead of Object.is.
Deduplication tests for objects, arrays, and Maps
packages/store/src/history.test.ts
Adds four tests: structurally equal objects with different references do not grow past; content changes do grow past and update present; structurally equal arrays do not grow past; custom equals function with Map state prevents duplicate past entries.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • Karanjot786/TermUI#669: Directly relates to the initial createHistoryStore implementation and history.test.ts coverage that this PR fixes by replacing Object.is with structural equality.

Suggested labels

type:bug, type:testing, area:store, quality:clean, level:intermediate

Suggested reviewers

  • Karanjot786

🐇 No more phantom past entries piling high,
Two { count: 0 } objects wave the same goodbye.
JSON.stringify checks what Object.is could not see,
And Maps get a custom equals, happy and free.
The history stays clean — hooray, it's a fix from me! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title mentions the main fix (replacing Object.is with structural equality) and relates directly to the changeset.
Description check ✅ Passed Description covers all required template sections: explanation of the bug, linked issue, package, type of change, checklist completion, and GSSoC participation with profile link.
Linked Issues check ✅ Passed PR fully addresses issue #1742: replaces Object.is with JSON.stringify structural equality, adds HistoryStoreOptions with optional equals comparator, includes comprehensive tests covering object/array deduplication and custom equality.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the deduplication bug: new interface, updated function signature, equality logic, and corresponding tests. No unrelated refactoring detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/store/src/history.test.ts (1)

125-139: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider adding a test for the Object.is fallback path.

The default equals function falls back to Object.is when JSON.stringify throws (e.g., for BigInt values). A test exercising this branch would improve confidence in the fallback behavior.

it('falls back to Object.is when JSON.stringify throws', () => {
    const bigIntStore = createHistoryStore({ value: BigInt(1) });
    const ref = bigIntStore.present;

    bigIntStore.set(ref); // same reference

    expect(bigIntStore.getHistory().past.length).toBe(0);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/store/src/history.test.ts` around lines 125 - 139, Add a new test
case in the history.test.ts file to verify the Object.is fallback path in
createHistoryStore. Create a test that initializes the store with a BigInt value
(which causes JSON.stringify to throw), stores a reference to the present state,
sets it back using the same reference, and asserts that the past history remains
empty to confirm Object.is comparison is working as the fallback behavior.
packages/store/src/history.ts (1)

70-76: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Note: JSON.stringify strips undefined values.

Objects like { a: undefined } serialize to "{}", making them equal to {}. This is standard JSON behavior but may surprise users expecting strict structural equality. Consider noting this in the JSDoc, or users who need stricter behavior can provide a custom equals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/store/src/history.ts` around lines 70 - 76, The default equals
function implementation does not document the behavior that JSON.stringify
strips undefined values, which can cause unexpected equality results (e.g., { a:
undefined } equals {}). Add JSDoc documentation to the equals option or the
containing function that clearly explains this JSON.stringify behavior and notes
that users requiring stricter structural equality can provide a custom equals
function to override the default behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/store/src/history.test.ts`:
- Around line 125-139: Add a new test case in the history.test.ts file to verify
the Object.is fallback path in createHistoryStore. Create a test that
initializes the store with a BigInt value (which causes JSON.stringify to
throw), stores a reference to the present state, sets it back using the same
reference, and asserts that the past history remains empty to confirm Object.is
comparison is working as the fallback behavior.

In `@packages/store/src/history.ts`:
- Around line 70-76: The default equals function implementation does not
document the behavior that JSON.stringify strips undefined values, which can
cause unexpected equality results (e.g., { a: undefined } equals {}). Add JSDoc
documentation to the equals option or the containing function that clearly
explains this JSON.stringify behavior and notes that users requiring stricter
structural equality can provide a custom equals function to override the default
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c0f99c75-ff33-495b-a609-399e67a51446

📥 Commits

Reviewing files that changed from the base of the PR and between 35c2213 and 3dbad4a.

📒 Files selected for processing (2)
  • packages/store/src/history.test.ts
  • packages/store/src/history.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug +10 pts. Bug fix. type:testing +10 pts. Tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: createHistoryStore uses Object.is for deduplication instead of structural equality

1 participant