-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
fix: stack-trace-based readonly validation #10464
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
Conversation
The readonly dev time validation results in false-negative object equality checks: The original object is proxified and thus comparisons could be between the unproxified and proxified version, which will always return false. Fixes #10372 There's also the problem that an object could be passed into a component but then passed upwards into shared state. At that point, it should no longer be readonly, but it's not possible to statically analyze these points. Fixes #10372 Lastly, the each block logic mutates an internal array and that also throws errors with the readonly logic. Fixes #10037
🦋 Changeset detectedLatest commit: 26455c3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Took me way too long to figure this out, but ownership needs to be a proxy concept rather than a signal concept. Everything fell into place after that realisation. Inheriting ownership now works |
If this is now a proxy concept rather than a signal concept, how does state on classes work with this validation? |
It doesn't, it has the same characteristics as the current proxy-based readonly validation, which we justified on the grounds that if you're passing around a custom class instance you probably do intend for consumers to interact with it. If we wanted to, we could make it work by augmenting the transformed class declaration to include a similar metadata object, and recurse into it. I didn't do that in this PR though |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the response. The code looks good. :)
Demo
I'm pretty sure I'll look at this in the morning with abject horror, but I thought the idea was at least interesting enough to share even if this PR is really just a proof of concept.
#10378 proposes removing the readonly validation that prevents components from mutating their props. I think it would be a terrible shame to lose this, and so I've been trying as best I can to come up with a suitable alternative.
Here's a rough description of what's happening here (note that all this only applies to dev mode):
1. add metadata to the transformed component
We wrap each component in these function calls:
push_module
andpop_module
capture stack traces, allowing us to determine the boundaries of each.svelte
file even if some bundling or other transformation has taken place (as is the case in the preview playground, for example).2. signals are given 'owners'
When creating a signal, we add the current component function to the set of the signal's owners.
When we pass a signal to a child with a binding...
...we add the child as another owner:
add_owner
causes adeep_read
of its first argument; anyget
that happens inside causesChild
to be added as an owner.3. capture stack traces during
set
Whenever a
set
happens, we capture a stack trace. Because we know the boundaries of each component, we can reliably determine whether the change originated inside the parent or child component, even if the actual mutation happens inside a function in a third module.If the most recent component in the stack trace is not among the signal's owners, then we know it has been mutated illegally.
(Importantly, if the child component mutates state via a callback prop passed from the parent component, the parent will be the most recent component in the stack trace, meaning the mutation is treated as safe regardless of any bindings.)
Lots still to do:
foo.bar = {...}
)bind:
with state that belonged to its parent (and which it does not co-own)Speaking of edge cases, there definitely are some. A stack trace can tell you which component file was the source of a mutation, but not which component instance, so if you have a case like this...
...then, thanks to the
bind:foo
,Child
will become an owner offoo
meaning that the first<Child>
component would be able to mutate it even though it isn't bound. This feels like something we could probably live with. There may be other edge cases that we can't live with.Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.Tests and linting
pnpm test
and lint the project withpnpm lint