-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
fix: use state
instead of source
in reactive classes
#16239
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
base: main
Are you sure you want to change the base?
Conversation
|
|
New idea I'll try to implement: store the active reaction in the constructor of the Map/Set and use a |
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.
Great work!
@@ -56,13 +56,17 @@ export class SvelteMap extends Map { | |||
#sources = new Map(); | |||
#version = state(0); | |||
#size = state(0); | |||
/**@type {Reaction | null} */ | |||
#initial_reaction = null; |
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.
Wondering if there are cases when map still lives, while the reaction could have been GCed and this would prevent it?
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.
Yeah I was wondering about that too...technically it shouldn't be the case because either there's no active reaction (it's outside of a derived/effect) or it's inside it but this means that when the derived/effect is no longer used the function should be GCd and everything inside it too...but I need to do a better check
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.
Maybe this can become initial_reaction = WeakSet(active_reaction)
and we can check with this.initial_reaction.has(active_reaction)
?
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.
Just for the sake of history, I've also discovered there is a WeakRef struct that could be alternative here. In case of any future issues might be a possible alternative implementation
Also tried installing this version locally, enabling runes mode forcefully and clicking through the interface. Looks like not hitting |
teardown(() => { | ||
this.#initial_reaction = null; | ||
}); |
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.
presumably if there's a situation that would create a memory leak with this (which I guess means smuggling a reference to the map to something outside the current reaction?), teardown
only mitigates it, since the reaction in question might never be torn down, if it's an unowned derived.
do we therefore need a way to run code as soon as the current reaction has finished updating? #16316
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.
The alternative is to use a weak ref to reference the reaction. Perf impact should be close to zero since it only applies once for new sources
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.
We could also use WeakRef, we were discussing with Simon this morning that this seems the perfect use case for it
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.
I've pushed a commit to use WeakRef
since it's way easier than handling #16316 for this edge case and this is kinda the perfect use case for it. We can always revert if we don't feel like going for it.
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.
Not sure I love the WeakRef solution tbh — it increases overall memory usage (since every map/set now has to have a WeakRef instance) and slows down every #source
call, since WeakRef is known to impact performance
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.
Consider it to be the same as previous values in teardown. It's not about if it's right or wrong, it's about the expectations and not making it confusing on the DX side of things
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.
That will not introduce a leak...to introduce a leak you would do some weird as Simon pointed out
It can only leak if you create the map inside effect context A and then pass it outside to another context, and context A is destroyed but not the context it's passed to
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.
Hm, not sure what I am missing here. Why it won't introduce a leak? As long as I hold to the map somewhere I hold to the $effect
scope, no?
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.
Duh I misread your code...yeah that will "leak" although only until foo
is not garbage collected.
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.
Yeah, so my point is that if I write to foo.bar
I do understand that the map is held until foo.bar
is garbage collected, but I definitely don't expect $effect
and everything related to it (dependencies, nodes and everything related to components) to leak as well.
Closes #16237
So the problem with using
source
is that:$effect
/$derived
that reads asource
created in itself will also depend on it...since normally state created in a reaction doesn't depend on itself this is counter intuitiveThis is a bit problematic in some cases because we create the sources lazily...in the case of
Map
for example we don't create asource
unless youget
orhas
orset
that first. However this means that if you create a newMap
in a derived ask if ithas
something, then you try to write to it in the same derived the newly createdsource
throws the error.For
set
it was as simple as changing source tostate
...however withget
andhas
it's a bit different: the effect needs to have a dependency on thesource
sostate
is out of question but then you don't want writes to it to throw. For that my solution was to add the source to thereaction_values
after theget
(only if the sources was newly created). For#read_all
it was a bit more complex and i had to create aWeakSet
for the newly created signals (not sure about memory/performance impact of this tho).I also changed the
source
's tostate
's inTweened
andSpring
since that also could've been problematic in cases like thissource
is also used insidecreateSubscriber
and I think it could have a problem there too but i need to check to be sure.It's also used inside
await
,each
and other blocks that "create" a value but i think for that it's probably fine.As I've said in the issue I'm not satisfied with this solution since it feels a bit hack-ish to add the dependency and still allow the user to write to it.
I also need to add a bunch of tests for this but wanted to give the code to @gyzerok
Before submitting the PR, please make sure you do the following
feat:
,fix:
,chore:
, ordocs:
.packages/svelte/src
, add a changeset (npx changeset
).Tests and linting
pnpm test
and lint the project withpnpm lint