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
Returning a Proxy from a Custom Element constructor #857
Comments
No. Custom element constructors need to operate on the |
@trusktr I used to find this (both in HTML and in regular ES) really frustrating. It took me a long time — and getting into implementation work — to really understand the “why” part of this (and relatedly, why some people say the behavior of isArray is broken rather than helpful, which used to baffle me). I recommend really digging in if you’re curious, both because it’s a super interesting subject and because one will learn why the denial of this request and similar ones isn’t a matter of people not wanting to help us, but rather that the concept we’re after is fundamentally at odds with the “physics” of JS, like asking for 2 + 2 to equal 7. That said, you might be interested in the fact that you can define a prototype which is a Proxy exotic object that implements |
Would you mind summarizing some bullet points? This totally works: class MyEl extends HTMLElement {
constructor() {
super();
Object.setPrototypeOf(
this,
new Proxy(Object.create(HTMLElement.prototype), {
get(...args) {
console.log("Muahaha.");
return undefined;
},
set(...args) {
console.log("Muahaha.");
return true;
}
})
);
}
}
customElements.define("my-el", MyEl);
const el = new MyEl();
document.body.appendChild(el); |
The element object is a special object created by browser internals. It has various kinds of state associated with it, e.g. via internal slots. The proxy object doesn’t have these slots. They’re not like properties; there is no object internal method for e.g. ‘getSlotValue’. They’re more like values in a WeakMap which is keyed by the object’s identity (the same model as private fields). There are various reasons why they could not be made to ‘pass through’ (I could try to explain this further, but I’m not sure I’m the right person to explain it effectively, as I barely understand certain issues there). But in any case, from the browser’s perspective, a proxy exotic object is an entirely different object (and it is, literally). It’s not a platform object or an element. In userland classes, we can associate private state with proxies freely with various mechanisms. But to do this, it has to be done after the fact — you need to actually have access to the new overridden object somewhere. The construction algorithm doesn’t operate in the right ‘direction’ to permit this: each derived constructor has an opportunity to perform a return override, but only more derived constructors will receive the new object as their Element construction is somewhat unusual when performed by the browser when processing HTML or on upgrade (as you were talking about in the other thread). In these cases, the agent does have a chance to see what ‘came out’ of the user constructor. But this isn’t the only path — custom elements are also independently constructable, in which case the new object doesn’t get back to the agent during the process; also, because an unupgraded instance is potentially out there, you’d now have to do something to replace that object, and you’d have leaked the proxy target at best — things could get pretty weird. |
I don't get that part. I always thought the unupgraded instance would be the same instance always used by user or engine, regardless if it is a target for a Proxy, or an item in an Array, or a key in a WeakMap. Can't the engine be made to read the internal |
I’m unsure I understand the first part of what you said there. The object which was in the DOM in this scenario was |
Can we change that part? |
What I meant is, the underlying target of the Proxy my custom element users have Can't the engine be made to access the target, or just ignore the Proxy, in the cases where the Proxy target is the same |
It feels limiting that the one time I try to use Proxy for a significant purpose happens to be with Custom Elements, because that's what I mostly work with is web-based UIs, and I can't do what I want to do, because of outdated architecture designs. |
This inconsistency across APIs in the browser is not a good thing. |
Proxies forwarding slots (or identity?) has a bunch of weird implications. The most obvious one is that you’d now have two objects floating around which both ‘are’ the same element yet are not the same object. But there are much more subtle issues which relate to the concepts of membranes and information side channels which I skipped over earlier because I don’t think I understand them well enough to actually explain them. However searching for some of those words together (membrane, proxy, etc) would likely turn up good info on the subject. It gets kinda heady though. In any case you’d still have the target leak issue on account of the unupgraded element (the target) being potentially accessed and closed over before the upgrade took place, and the issue of replacing that target everywhere it gets exposed in the DOM with the new object, e.g. in NodeList instances. I would say that this isn’t really an inconsistency, even though I feel what you’re saying and agree that it would be great if we had some mechanism to achieve this stuff. It precludes authoring any classes that inherit from platform interfaces which in turn implement stuff like Web IDL getter behaviors, which is limiting when it comes to implementing certain platform APIs/polyfills in the ES layer, a subject I’m super interested in. But there’s a huge amount of thought that went into this design and its limitations stem from the need to ensure certain truths hold (it’s actually closely related to some of the stuff in that other thread, about making sure the ES object model continues to permit ... I don’t know the right word for it, ‘zones of control’ is the best I can think of). |
I expect a membrane, or whatever I implement with a Proxy, on an element to be for userland. If the engine needs to modify The use cases for Proxy (in my case) are to handle users' set/get on the instance. I already know that the HTML engine isn't going to arbitrarily set properties that I care about on my custom element instances. If the engine does anything with my instances, it will call life cycle hooks in response to user actions, which signal for me to manipulate my instance in response to the action. I don't see how the engine using the underlying [[Target]] would be an issue, unless that [[Target]] is not the same object that the engine created.
What leak? The reference to the object created by the HTML engine (which happens to be the target in a proxy in userland code) doesn't change. I'm imagining that the engine will upgrade the underlying Regardless of Proxy, people need to at some point handle a future upgraded instance (whose reference will be the same) in either the custom element It won't affect the Proxy, because the Proxy traps will be called when the user of the Proxy performs an action like getting or setting a property on the Proxy, at which point the property or method will either exist, or it won't, depending on whether the underlying [[Target]] that the HTML engine references is upgraded yet or not. We can close over non-upgraded instances today, as is, and manipulate them before upgrade, and the problem isn't any different with or without Proxies, the way I see it. |
I’m unsure, but possibly I think there may be a misunderstanding here about the upgrade process? When accessed prior to upgrade, the instance isn’t a different object from what’s around after upgrade. I.e. there’s no before element vs after element — the
Yes, it would have to; the proxy doesn’t exist at that point.
Leaking isn’t about the upgrade itself — it’s about the target object having been exposed globally in the DOM prior to the return override which would take place after upgrade.
I’m not sure what’s meant by this. The reference will be the same when/where? |
That's what I was saying all along. So the same reference will be in any WeakMap, Array, Proxry target, jQuery cache, etc, and the upgrade process won't break that. If Proxies were allowed in the engine, then the engine could be a special case where it can access
I'm proposing something a little different: the only allowed return override should be Proxies that wrap the object created by the HTML engine. The DOM would not exchange engine's element reference with any userland constructor return value. Userland code would reference a Proxy, the internal engine would continue to reference the [[Target]] of any Proxy. So the target won't leak, because the target is what the engine currently would have a reference to, if Proxies were allowed. To sum it up:
|
Hmm, but if what I described were the case (if Proxies were allowed, and DOM APIs operated only on the [[Target]]), then an issue I foresee would be that if someone else used |
If the Custom Element spec had disallowed upgrades, then we'd be in good shape: the engine could rely on Proxied instances (assuming [[Target]] matches the engine's internal object, otherwise error) because, for internal slots, the engine could read from the target object directly. It'd be nice if a warning was thrown to console when elements already exist at the time of definition. Then developers would be forced to put element definitions up front. It would guarantee correctness (and prevent all existing upgrade issues). Anyways, it's just a dream. I'll have to find another way to implement my multiple inheritance tool with Custom Element classes. It'll have to be a Proxy-on-prototypes-only approach, but the |
Yes, I think we’re on the same page now regarding what I was referring to as a leak. What can be achieved with only an inherited proxy is limited, yeah; I wasn’t sure whether that recipe would do you any good or not. Proxies don’t afford anything that an ordinary object’s internal methods would not have — even a platform-defined exotic object couldn’t bypass that, it’s the signature of the real internal [[HasProperty]] — but then, they wouldn’t need to, since they could make the instance itself exotic instead. Looking at your use of proxies in that lib, I’d note that you can achieve run-multiple-real-constructors-with-one-instance without using proxies if it’s okay to require those constructors to extend a special meta class. But I didn’t dig in too far, so I’m not sure what other stuff proxies may be facilitating for this lib. override chaining (unsophisticated demo)function synthesize(...constructors) {
return class {
constructor() {
for (const constructor of constructors) {
Reflect.construct(constructor, [ this ]);
}
return this;
}
};
}
function superOverride(instance) {
return instance;
}
////////////////////////////////////
const ABC = synthesize(
class A extends superOverride {
a = 1;
},
class B extends superOverride {
b = 2;
},
class C extends superOverride {
c = 3;
}
);
console.log(new ABC); // { a: 1, b: 2, c: 3 } |
@bathos One thing that I want to facilitate with the lib is importing classes from anywhere and extending from them, without needing to modify their source code, f.e. import {multiple} from "lowclass"
import {EventEmitter} from "events" // does not extend special meta class
import {Something} from "something" // does not extend special meta class
class Foo {...}
class Bar extends multiple(HTMLElement, Foo, EventEmitter, Something) {...} With class-factory mixins, or with constructors having to extend a meta class, there would be no way to import an arbitrary set of classes from anywhere and extend from all of them because they aren't wrapped in functions, or they don't extend from the meta class (they may extend from some other class), respectively. |
Unfortunately, the following causes an error:
Can Custom Elements API be upgraded to support Proxied instances?
The text was updated successfully, but these errors were encountered: