dom icon indicating copy to clipboard operation
dom copied to clipboard

`mutationObserver.disconnect({ flush: true })`

Open LeaVerou opened this issue 1 year ago • 10 comments

What problem are you trying to solve?

Calling mutationObserver.disconnect() while forgetting to handle any pending records is a common footgun.

What solutions exist today?

Having to write boilerplate like:

let records = mutationObserver.takeRecords();
if (records.length > 0) {
	callback(records);
}
mutationObserver.disconnect();

Every time you want to stop observing is quite repetitive, and it cannot be done with just a reference to the mutation observer, since it requires a reference to its callback too.

How would you solve it?

Add a dictionary argument to disconnect() with a flush option (name TBB) that does exactly this.

Anything else?

No response

LeaVerou avatar Apr 27 '24 01:04 LeaVerou

as one that has been using MO forever (polyfills included) I never even thought the leaky records would be a problem but I fully agree with this issue concerns and I also need to likely update all my libraries heavily based on MO to guarantee something not observed anymore won't be processed after a disconnect happens.

Thanks for considering this bug and raising its importance too, if needed.

WebReflection avatar Apr 28 '24 15:04 WebReflection

Definitely interesting and I think it'd be nice to have this as a feature of MutationObserver...from a topical assessment it doesn't seem like this would break anything. I agree that takeRecords() is quite clunky and could use some modernization.

I'm open to implementing something or helping out with the specs if this issue ready for those!

qu1chu5 avatar Oct 02 '24 17:10 qu1chu5

I actually wonder if this might be something we could simply fix. Code that follows the pattern in my first comment won't break, it will simply take the records before disconnect() is called. And the rest is already broken — I'd wager it's highly unlikely that code depends on pending records not being taken.

If not, another solution is to introduce a different method, and deprecate disconnect(). Though this is a conventional name at this point that other observers share too…

@annevk What do you think?

LeaVerou avatar Oct 04 '24 05:10 LeaVerou

I'm not sure. @smaug---- and @rniwa are probably better equipped to judge the impact of such a change.

annevk avatar Oct 04 '24 07:10 annevk

Adding option to disconnect seems like a sensible approach. Changing the behavior of disconnect at this point is likely going to hit some web compatibility issues (this API has been shipping for 10+ years) so I'd rather not.

rniwa avatar Oct 04 '24 07:10 rniwa

the rest is already broken

The rest is not already broken. disconnect() was designed to stop observing anything, very explicit. No different to removeEventListener. (There might be an event queued in a task, and you won't get that event if removeEventListener is called).

If we add a dictionary to disconenct() for flush, I wonder if we want also explicit flush() which is like takeRecords() but calls the callback.

smaug---- avatar Oct 04 '24 10:10 smaug----

@rniwa

Adding option to disconnect seems like a sensible approach. Changing the behavior of disconnect at this point is likely going to hit some web compatibility issues (this API has been shipping for 10+ years) so I'd rather not.

We can't keep adding API surface for every fix. How would sites be depending on the behavior of pending records not being taken?

Anyhow, if the compat risk is deemed too high I think a new method might be better, so we can gradually steer people towards using that, rather than being stuck with a method where you need to pass an optional parameter in 99.9% of the time (like cloneNode(true)).

@smaug----

The rest is not already broken. disconnect() was designed to stop observing anything, very explicit. No different to removeEventListener. (There might be an event queued in a task, and you won't get that event if removeEventListener is called).

How frequently does that happen though? It sounds like an extremely rare race condition, whereas what I’m talking about here happens way more.

If we add a dictionary to disconenct() for flush, I wonder if we want also explicit flush() which is like takeRecords() but calls the callback.

That seems like a good idea!

LeaVerou avatar Oct 04 '24 13:10 LeaVerou

:+1: for .flush() and dare I say .detach(element) or .unobserve(node) would be a nice extra things to the MO API.

edit if there's any doubt around why the flush() is desirable, people just bootstrap MO with an arrow function, they don't necessarily make that a scoped reference, and once they do that, there's no way to trigger that explicitly so that takeRecords() is currently a libraries/frameworks thing to do only, not the norm for developers. flush() that automatically triggers the registered callback as in thatCallback(mo.takeRecords()) would be a super easy, clean, and cool way to reason about the API.

edit 2 dare I say flush() would make takeRecords() almost useless ... but there are places where explicit records to pre-process might be desired.

WebReflection avatar Oct 04 '24 13:10 WebReflection

@LeaVerou what do you envision this addition looking like? Is it better to have redundant options (like giving developers the choice between passing in a dictionary to takeRecords() to automate this process when disconnect() gets called or calling flush() manually)? Or is this something should we only implement in one way?

@smaug---- Do you know why disconnect() was designed this way? I'm curious if there's any case where a developer might want to use takeRecords() with disconnect(), then later in the codebase call observe() and disconnect() without takeRecords() using the same MutationObserver object. Seems like an unlikely scenario but if there's a use case for doing this I think the redundancy described above might be the best way to go about this feature addition.

qu1chu5 avatar Oct 06 '24 00:10 qu1chu5

The proposed disconnect option would also be useful to me — and a flush() method even moreso, for the reasons @WebReflection described. Every time I’ve ever used takeRecords(), it was specifically to do what flush() would do, and its absence presently necessitates keeping a common function reference around for both scenarios, which has seemed like awkward boilerplate.

(And yep: I’d also love an unobserve-specific-node-only method — though I imagine that would probably merit its own issue.)

bhathos avatar Oct 06 '24 04:10 bhathos