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
Infinite recursion with arrays #8
Comments
From the limited amount I've been able to figure out, it seems like the store-handling code may be recursively wrapping arrays in proxies on dereference. Honestly, I don't know what's happening. |
Do you have a standalone repro outside of Stackblitz, preferably something that I can execute in Node? It you write |
I don't currently have a standalone repro outside of codesandbox at the moment, but I am experiencing similar issues in my app, which is not in codesandbox. I've updated the codesandbox to show that its But if we unwrap the store and then try to log it, it freezes in the same way. I think this is because the unwrap isn't deep, it doesn't go down into the array and unwrap the proxy in there. So when it is logged out, we are left with the same problem we had originally. |
Basically I've no idea what the actual issue is here, if you can make a repro that I can run in Node I'll look into it 👍 |
Okay. That's understandable. Something about codesandbox Digging into the issue locally, I've tracked my problem down to something about the way the serialization code in the So that appears to be two cases of widely-used serialization code (codesandbox |
Here's a repro repo https://github.com/theSherwood/voby-store-repro Notice that the issue happens with proxies and unwrapped proxies, but not with the circular object that was never proxified. Edit: Also notice that the limits which are put in place to prevent infinite recursion are somehow never triggered but the page will hang after clicking |
In general it's not really possible to guard against buggy code though. I see the issue now, basically when one does this: state.nested.arr = [...state.nested.arr, state.nested]; Or just this, presumably: state.nested.arr = [state.nested]; Then what happens is that internally a proxy gets set on the unproxied object, which causes There are potentially many ways to try to address this, none ideal:
I'm not sure what the best way to address this would be, I need to think about it 🤔 What do you think should happen? |
Yeah, that's true, but I don't think that's what's going on in this case though. That serialization code isn't buggy for circular objects. Maybe you're saying that the bug is to apply that serialization code to the proxies. But my impression is that if a proxy can't be treated more or less like the object it's proxying, that's surprising. I would expect instances of classes to not behave as plain objects, but I sort of expect everything that works on a plain object to also work on a proxy. If that's not the case, I think there should be warning signs.
I thought it already unproxied set values, just not with array items? Have I got that wrong?
That was my first thought, But then I'm not sure how that would work. You'd have to modify or make copies of arrays when doing an unwrap (if it has proxy items) and that seems really surprising. But it seems like you'd have that same issue with any deep unwrap, which seems like a problem.
If you try to deeply unwrap
This is simplest but means losing reactivity when reaching through a nested array, which seems less than ideal.
I'm not sure. It's a tricky problem. My understanding is that if array methods were proxied, then unwrapping could still be constant time without losing the nested reactivity. Is that correct? |
I guess that's a little surprising, and there should be warning signs, but I guess they should be on MDN or something? Like a proxied object is not triple-equal to the unproxied object. structuredClone throws for proxies. postMessage also throws. It's not Oby's place to document this stuff probably, though some warnings can be added to the readme if they are useful of course.
Yes it already unproxies set values, but not deeply. In your code the proxy is found "deep" inside the array, so that doesn't get unproxied.
Yeah that's another problem with deep unwrapping, especially if done by default. You deep unwrap twice and the two objects don't match one another. That's how Solid works by the way.
Losing reactivity how? This option should JustWork™️, it "just" requires some understanding of this problem and manual error-prone code that unwraps values being set.
I'm not sure what you mean, it seems orthogonal to the problem at hand 🤔 array methods are already proxied. |
This is surprising to me (but in a good way). I just tested this out and it works fine. I don't know why I thought it didn't.
Gotcha. I think that you are right that it shouldn't unproxy deeply by default. I think I am seeing where my surprise is coming from. With Solid proxying/unproxying deeply, if I set an array on a proxy, the elements are proxied/unproxied appropriately automatically. Oby does not do that. But if you have a proxy with an array and push something onto that array, it is proxied/unproxied appropriately. So there's a difference in Oby with proxy handling depending on whether you replace the array with a new array or whether you modify the array in place. This a pretty surprising difference but makes sense given the assumptions of the library. It's just going to be a potentially sharp edge if you're coming from Solid. Maybe a "Migrating from Solid" page would be helpful here. Part of me wonders whether array items should be proxied/unproxied by default if you set an array in a proxy. So not "deep" per se, but 1 layer in. That doesn't feel great as a solution but it does cover what may be a common case. Given the severity of the bug (I lost several hours to this), something ought to be done. Maybe a "migrating from Solid" page is enough. Maybe proxying/unproxying array contents if an array is set causes other surprises later. I really don't know. Tangential to this, the different philosophies around stores between oby and solid lead me to expect oby to perform worse with deep stores that get accessed repeatedly because the nested objects have to be reproxied lazily at every access. Is this correct or do you use a WeakMap to cache proxies? Edit: for clarity |
Re something ought to be done: I think maybe what should happen is that a warning for this should added to the readme, explaining how setting a value that has a proxy inside it can lead to issues with unwrapping, and potentially also a Re migration from Solid: maybe that's something for a v1 release, or when we have a website, at the moment I don't really care about having more people using Voby/Oby, I don't have the bandwidth to write proper docs, a website, guides, and stuff like that. Also I can't write "migration from *" guides for every framework, and I basically consider Solid's stores unusable, there are so many issues with them that they are basically unusable really, imo. Re performance: there's a WeakMap to cache proxies, we need something like that or the implementation wouldn't even be correct, besides being slow. I had benchmarked Voby's stores against Solid's stores at one point, and Voby's stores were faster. But the comparison is kinda pointless imo because Voby's stores to the best of my knowledge actually work, Solid's don't really, so the comparison doesn't make too much sense, like I can make anything as fast as you want if I can make it incorrect. |
Regarding the 1 level unwrapping: depending on how you look at it it feels like a worst of both worlds, or a best of both worlds, compromise. From my point of view unproxying at 1 level is both potentially way slower than the constant time unproxying, and also doesn't solve the problem in general. From another point of view I guess it could make more code work out of the box with no user-level changes. I don't really like it as a solution. |
What you've outlined seems pretty reasonable. I think a big warning in the readme/docs would be very handy. This issue has cost me a lot of time because the nature of the bug is so weird. It's seemingly recursive but it never blows the stack. And I think there might be something in the oby code swallowing errors because when I was playing around with it locally, I threw errors from the oby code when the it went through too many iterations of
I totally get that. Solid's stores have bitten me repeatedly. Especially as they've changed several times.
That makes sense.
I was only thinking about this for when an array gets set on a store field. Any other unwrapping would be constant-time. But I agree that it is surprising and could lead users to think that the unwrapping is going to be deep when it isn't. At least with no unwrapping of array items (status quo), it's very clear that there's no unwrapping beyond what you see. Thanks for all your time on this. Oby's batching is chef's kiss by the way. |
I was throwing from within oby library code. From within |
An option that just occurred to me is to have a debug mode that catches this infinite loop. Not clear to me how much of a lift that would be, though. |
I'm going to add a warning to the readme about this. I'm not adding the In the meantime if anybody needs a import {store} from 'oby';
const isProxiable = ( value: unknown ): boolean => {
if ( value === null || typeof value !== 'object' ) return false;
if ( isArray ( value ) ) return true;
const prototype = Object.getPrototypeOf ( value );
if ( prototype === null ) return true;
return ( Object.getPrototypeOf ( prototype ) === null );
};
const deepUnwrap = <T> ( value: T ): T => {
const unwrapped = store.unwrap ( value );
if ( isProxiable ( value ) ) {
if ( Array.isArray ( value ) ) {
const clone = new Array ( value.length ) as T;
for ( let i = 0, l = value.length; i < l; i++ ) {
clone[i] = deepUnwrap ( value[i] );
}
return clone;
} else {
const clone = {} as T;
for ( const key in value ) {
clone[key] = deepUnwrap ( value[key] );
}
return clone;
}
} else {
return unwrapped;
}
}; |
@theSherwood are you still using Oby? Just curious |
I am currently still using it. The batching/tick behavior is pretty essential to the current iteration of my app. Though I've recently had some insights that might let me take over the tick behavior that would let me use alternate renderers. But I'm not in a rush to switch. |
@fabiospampinato
I've been running into a lot of difficult-to-debug issues around infinite loops involving arrays and circular references. Here's a sandbox with a sample of what I'm talking about:
https://codesandbox.io/s/voby-demo-store-counter-forked-r225g4?file=/src/index.tsx
(click the
+
a couple times and the app will hang)This has been a bit difficult in my app because Solid handles these circular references in arrays just fine and I have data of a similar shape cropping up in numerous places in my app. So this looks to me like a bug, but what do you think? Are these cases something that voby/oby should handle? Or does there just need to be a big warning sign in the readme?
The text was updated successfully, but these errors were encountered: