Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update history when a state property is set to undefined #14

Merged
merged 9 commits into from
Apr 28, 2024

Conversation

fabio-grieco
Copy link
Contributor

Fixes #13

Comment on lines 333 to 338
if (state.value.count === 1) {
state.value.foo = undefined;
} else {
state.value.foo = 'bar';
}
state.value.count++;
Copy link
Collaborator

@lwhiteley lwhiteley Apr 25, 2024

Choose a reason for hiding this comment

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

I'm not certain what this test is trying to prove.. all changes within the same execution scope will cause a single history change as far as I know. This includes increasing the counter.

Can you provide a simpler test that sets a value from defined to undefined.

You can also use the delete keyword to remove a property on the object as well

I would be interested to see both scenarios. And if they work or not without the changes made to the library

Ensure to add an await before the expectation is confirmed to simulate a change in execution context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I simplified the test case and I added another test case to check the behavior with delete.

@@ -230,7 +233,7 @@ export function proxyWithHistory<V>(
if (proxyObject.canUndo()) {
proxyObject.history.wip = proxyObject.clone(
proxyObject.history.nodes[--proxyObject.history.index]?.snapshot
);
) ?? EmptyWIP
Copy link
Collaborator

@lwhiteley lwhiteley Apr 25, 2024

Choose a reason for hiding this comment

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

I'm unsure what the expectation is. The issue says that the value is set to undefined and no history change is reflected.. but this is setting the value to an empty object when null or undefined is found.

Seems like a data integrity issue.

Why not just set the value externally from the lib to an empty object if that's what's required in that use case?

What is the expectation here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I'm understandin correctly, the method shouldSaveHistory returns true when the value changed and when the new value is different than the history wip.

Since by default (when empty) the history wip is undefined, when setting a value to undefined the above method returns false and the history isn't updated. By setting the default history wip value to an empty object, when the value has changed and the history wip is empty the above method would return true, so setting a value to undefined would work as expected.

Using an empty object ensures that it is equal (===) only to itself, so even setting the new value to an empty object would work as expected, but that is not the use case I was trying to address.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Still don't really get the point..if the value is undefined and you try to set it to undefined then there should be no history change..

But if it's set then that means it's not undefined.

And if it sees undefined then undefined will not be equal to wip..

So what am I missing?

In theory there is no benefit to changing it to an empty object.

Do your test cases pass without these changes?

Copy link
Contributor Author

@fabio-grieco fabio-grieco Apr 25, 2024

Choose a reason for hiding this comment

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

It's not the value that is undefined but the history wip property.

The first test case fails, the second one (delete) succeeds. If you run the first test case without the changes, you can see that when invoking setValueToUndefined() a state value is set from "bar" to undefined and the history is not updated.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks.

I'll check it in detail later

Copy link
Collaborator

@lwhiteley lwhiteley Apr 27, 2024

Choose a reason for hiding this comment

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

    it('should update history when property is deleted', async () => {
      const state = proxyWithHistory<
        Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
      >({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });

      await Promise.resolve();
      expect(state.value.prop0).toEqual(0);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(1);

      delete state.value.prop0;

      await Promise.resolve();

      expect(state.value.prop0).toEqual(undefined);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(2);
    });

    it('should update history when property is set to undefined', async () => {
      const state = proxyWithHistory<
        Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
      >({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });

      await Promise.resolve();
      expect(state.value.prop0).toEqual(0);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(1);

      state.value.prop0 = undefined;

      await Promise.resolve();

      expect(state.value.prop0).toEqual(undefined);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(2);
    });

i used these test cases without your changes to understand the issue a little better in isolation of any other property changes.

first test case passes and the second one doesnt.

so thanks i have a clearer understanding

Copy link
Contributor Author

@fabio-grieco fabio-grieco Apr 28, 2024

Choose a reason for hiding this comment

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

Nice, I added your your test cases to the vanilla suite and removed mine from react suite.

@lwhiteley lwhiteley changed the title Fix: update history when a state property is set to undefined fix: update history when a state property is set to undefined Apr 25, 2024
Comment on lines 359 to 390
await screen.findByText('count: 0');

fireEvent.click(screen.getByText('inc'));
await screen.findByText('count: 1');

fireEvent.click(screen.getByText('inc'));
await screen.findByText('count: 2');

fireEvent.click(screen.getByText('inc'));
await screen.findByText('count: 3');

fireEvent.click(screen.getByText('undo'));
await screen.findByText('count: 2');

fireEvent.click(screen.getByText('redo'));
await screen.findByText('count: 3');

fireEvent.click(screen.getByText('undo'));
await screen.findByText('count: 2');

fireEvent.click(screen.getByText('undo'));
await screen.findByText('count: 1');

fireEvent.click(screen.getByText('undo'));
await screen.findByText('count: 0');

fireEvent.click(screen.getByText('inc'));
await screen.findByText('count: 1');

fireEvent.click(screen.getByText('undo'));
await screen.findByText('count: 0');
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

test title is : should update history when a state property is set to undefined

but there is no confirmation of what its trying to test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added more meaningful expectations.

@@ -230,7 +233,7 @@ export function proxyWithHistory<V>(
if (proxyObject.canUndo()) {
proxyObject.history.wip = proxyObject.clone(
proxyObject.history.nodes[--proxyObject.history.index]?.snapshot
);
) ?? EmptyWIP
Copy link
Collaborator

@lwhiteley lwhiteley Apr 27, 2024

Choose a reason for hiding this comment

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

    it('should update history when property is deleted', async () => {
      const state = proxyWithHistory<
        Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
      >({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });

      await Promise.resolve();
      expect(state.value.prop0).toEqual(0);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(1);

      delete state.value.prop0;

      await Promise.resolve();

      expect(state.value.prop0).toEqual(undefined);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(2);
    });

    it('should update history when property is set to undefined', async () => {
      const state = proxyWithHistory<
        Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
      >({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });

      await Promise.resolve();
      expect(state.value.prop0).toEqual(0);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(1);

      state.value.prop0 = undefined;

      await Promise.resolve();

      expect(state.value.prop0).toEqual(undefined);
      expect(state.value.prop1).toEqual(1);
      expect(state.value.prop2).toEqual(2);
      expect(state.value.prop3).toEqual(3);
      expect(state.history.nodes.length).toEqual(2);
    });

i used these test cases without your changes to understand the issue a little better in isolation of any other property changes.

first test case passes and the second one doesnt.

so thanks i have a clearer understanding

Comment on lines 26 to 27
type EmptyWIP = Record<string, never>;
const EmptyWIP: EmptyWIP = {};
Copy link
Collaborator

@lwhiteley lwhiteley Apr 27, 2024

Choose a reason for hiding this comment

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

i still have a problem with the semantics here so how about we use a Symbol and more standard naming convention

Suggested change
type EmptyWIP = Record<string, never>;
const EmptyWIP: EmptyWIP = {};
const EMPTY_WIP = Symbol('valtio-history-wip-empty');

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I fixed the naming case and used a Symbol.

export type History<T> = {
/**
* field for holding sandbox changes; used to avoid infinite loops
*/
wip?: Snapshot<T>;
wip: Snapshot<T> | EmptyWIP;
Copy link
Collaborator

Choose a reason for hiding this comment

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

given we use this once

Suggested change
wip: Snapshot<T> | EmptyWIP;
wip: Snapshot<T> | typeof EMPTY_WIP;

@@ -134,7 +137,7 @@ export function proxyWithHistory<V>(
* - history.wip: field for holding sandbox changes; used to avoid infinite loops<br>
*/
history: ref<History<V>>({
wip: undefined, // to avoid infinite loop
wip: EmptyWIP, // to avoid infinite loop
Copy link
Collaborator

Choose a reason for hiding this comment

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

also ensure the others are renamed

Suggested change
wip: EmptyWIP, // to avoid infinite loop
wip: EMPTY_WIP, // to avoid infinite loop

@@ -1,6 +1,6 @@
{
"name": "valtio-history",
"version": "0.3.2",
"version": "0.3.3",
Copy link
Collaborator

Choose a reason for hiding this comment

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

you dont need to adjust this.. its automated when releasing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, rolled back

@lwhiteley
Copy link
Collaborator

you will need to run

pnpm nx format:write

otherwise looks good

@fabio-grieco
Copy link
Contributor Author

Formatting applied 😊

@lwhiteley
Copy link
Collaborator

lwhiteley commented Apr 28, 2024

Thanks Fabio for finding and fixing this issue

I'll release it later

@lwhiteley lwhiteley merged commit ec38985 into valtiojs:main Apr 28, 2024
1 check passed
@lwhiteley
Copy link
Collaborator

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.

History not updating when a state property is set to undefined
2 participants