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

Throw when array and object deletion values do not match current version #23

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

alecgibson
Copy link
Contributor

This change adds validation to array and object deletion. If the values
provided in either ld or od do not match the current value, then
apply will throw. It will also throw if oi overwrites an existing
value without providing od.

The primary motivation of this change is to ensure that all submitted
ops remain reversible. At the moment, the following series of ops is
possible:

  • start with { foo: 'bar' }
  • apply this op: { p: ['foo'], oi: 'baz' }
  • ...resulting in { foo: 'baz' }
  • invert the previous op: { p: ['foo'], od: 'baz' }
  • and apply the inverted op: {}

When I apply, invert and apply, I should always wind up where I started,
but in this case I clearly do not.

By ensuring that the od matches the current value, we make sure that
all ops remain reversible.

Deep equality adds a dependency on fast-deep-equal.

@alecgibson
Copy link
Contributor Author

This should address the issue I raised on this thread: #16 (comment)

@josephg
Copy link
Member

josephg commented Apr 10, 2019

Hi! Thanks for your PR! Sorry its taken so long for anyone to get back to you on this.

I like this change; but I don't want to break anyone's code. Enforcing this would be a breaking change in this library. I'd like to have some sort of "strict mode" which actually checks this stuff, rather than just checking all the time.

It might instead be better to have a method which accepts a document and an operation and canonicalizes the operation, filling in the op remove fields based on the contents of the document.

@nornagon do you want to chime in on this?

@alecgibson
Copy link
Contributor Author

Happy to hide this behind a flag if you want it non-breaking. What would be the best way to do that? Also, for what it's worth, I've been use this change in a fork on a production environment for a while now, so it's also pretty battle-tested.

@pypmannetjies
Copy link

Any feedback on the above suggestion?

@alecgibson
Copy link
Contributor Author

@josephg / @nornagon is this PR good to go?

@nornagon
Copy link
Contributor

Hm, I'm a little hesitant here as this adds a possibly significant performance issue, and further, adds a dependency, which we are hitherto without.

Possible alternative: check === only? I don't see a good use case for constructing delete ops with anything other than a direct reference from the snapshot.

@alecgibson
Copy link
Contributor Author

We can't use === because by the time the op has been serialized, sent over the wire, deserialised, transformed and even cloned within json0 itself, any references will have been well and truly destroyed.

I'm not sure I understand the aversion to adding dependencies; deep equality is a hard problem, and libraries are there to help us solve hard problems. Admittedly as-is, it would increase the package size by ~17%, but the actual bones of the thing are small and (if we remove ES6/React-related code), we could even copy the whole function into json0 at the cost of ~40 lines of code if you're particularly concerned about package size (the library is under MIT).

In terms of performance, I'm happy to do any benchmarking you'd need. The library itself already boasts some impressive benchmarks, and we've been using json0 with deep equality checking in Production for 3 years now and we've not run into any noticeable issues.

If this functionality is truly something you don't want in json0, that's fine. I just think it's a shame, given:

  • there are todos in the code to add this functionality
  • without this, the type isn't guaranteed to be invertible
  • in my personal experience, some frontend JS frameworks have been quite naughty about mutating objects they don't own (I know AngularJS 1 used to add $$hash keys to our ShareDB doc data, which we had to make sure we stripped out when setting od)
  • these checks have helped us catch a bunch of bugs, both in our own code, as well as the path issue I recently raised

@nornagon
Copy link
Contributor

17%? that's a bit surprising, the code of fast-deep-equal looks smaller than that. + it looks like if we don't include the react/es6 versions of it, we won't get any unnecessary checks for weird react junk or Sets/Maps/etc. which we don't expect in any case.

If you've used this in production and both (a) haven't found performance issues with it and (b) it's discovered real bugs for you, then that seems like good justification to merge this. Just want to get clarification on the code size thing :)

@alecgibson
Copy link
Contributor Author

17% is hopefully a worst-case — I based it on the unpackaged size quoted on npm.com (13kB) without any tree-shaking. It's hard to actually say the "real" impact, just because it depends on the tree-shaking of the consumer (eg if they're already using fast-deep-equals, then there's no impact at all).

(As a side-note, we're currently packaging the test/ folder, which we can exclude if we want to get the package size down further)

This change adds validation to array and object deletion. If the values
provided in either `ld` or `od` do not match the current value, then
`apply` will `throw`. It will also throw if `oi` overwrites an existing
value without providing `od`.

The primary motivation of this change is to ensure that all submitted
ops remain reversible. At the moment, the following series of ops is
possible:

  - start with `{ foo: 'bar' }`
  - `apply` this op: `{ p: ['foo'], oi: 'baz' }`
  - ...resulting in `{ foo: 'baz' }`
  - `invert` the previous op: `{ p: ['foo'], od: 'baz' }`
  - and `apply` the inverted op: `{}`

When I apply, invert and apply, I should always wind up where I started,
but in this case I clearly do not.

By ensuring that the `od` matches the current value, we make sure that
all ops remain reversible.

Deep equality adds a dependency on [`fast-deep-equal`][1].

[1]: https://github.com/epoberezkin/fast-deep-equal
@alecgibson
Copy link
Contributor Author

(NB: I just noticed that the fast-deep-equal package has received a major bump since I first opened this PR, so I've bumped it. The breaks don't affect us).

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.

4 participants