-
Notifications
You must be signed in to change notification settings - Fork 13
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
[context] Some concerns, and an idea of how they could be addressed #49
Comments
I generally quite like this proposal, but one major concern that would need some prototyping and testing to evaluate is the cost of EventTarget based implementation of change events. The |
@benjamind Only just now have I found #19 and seen you write, two years ago, about the cost of events. You say that such a proposal needs to come with performance testing, but it seems like you already have a good idea of what the results of that testing would be: events are more expensive. But I don't think that means we shouldn't use them. As a compromise, providers could have a Events are the API for communication in the DOM. Providers are effectively already It seems strange that we would deliberately avoid using events and manually implement our own That said, the compromise approach I mentioned would still be a big improvement, I believe. For what it's worth, I do believe that |
I'd like to understand what the actual supposed problems are with the current protocol is and how the change is an improvement.
Why not? This is a subjective claim that could use some more backing. The callback is how the event listener is able to send information back to the event dispatcher. There are other ways to do this for sure, such as a simple property, but the callback is uniform across both the single request and subscription cases, which I think is a major benefit. Even base events have similar ways of send data from listeners to dispatcher:
This is very subjective too. Odd to whom? Again, the callback is a way for listener to send data to the dispatcher. One of the things that may need to be sent is the unsubscribe function. I don't find this odd.
You showed exactly how.
Consumers can already do this if they don't receive a call to the callback, like you showed. I would like to see claims of problems be more concrete. Like "can't do X", "performance is slow", "too much memory", etc. Right now it looks like your proposal is roughly similar in terms of capabilities, so I'd also like to see how it's better. I don't immediately see that it is. I do actually see important ways in which it's limited in compared to the current protocol. Most critically it creates a tighter coupling between the consumer and provider objects, such that it would be very difficult (if not completely unsupported) to dynamically change the provider of subscriptions. This is something that has come up in real-world usage of the protocol because of the dynamic nature of the DOM tree and custom element upgrades:
The loose coupling between consumer and provider of the current protocol is a huge advantage here. The semantics of the |
Fortunately I spent several paragraphs trying to articulate exactly this. I must tell you with respect that it does not feel like you've actually cared to listen to most of the arguments I was trying to make, which is really very disheartening as an outsider interested in advancing web components. I'm very happy to have it explained to me why I'm wrong, but this just feels passive-aggressive. I am not trying to argue that there is something the protocol should do which it currently doesn't; I'm trying to argue that, in my opinion, the design is confusing, is needlessly complex, has notable pitfalls, and poorly represents the semantics of what it's trying to do. If you're not interested in concerns of this nature, that's fine, but I'd prefer you to just say that.
I know. I realised this by myself. I explicitly acknowledged that this was the major advantage of the existing approach. It feels like you didn't even read this:
In this section I tried to outline multiple issues with using the callback when not subscribing, including what I think is the biggest flaw with the whole protocol (as I said), which is that it's possible for 'bad actor' providers to exist. It's not apparent that you listened to anything I said here. I made no mention of a provider returning a context value to a consumer using a "simple property".
You just haven't engaged with any of the arguments I tried to make here. Edit: As with the above instance with the callback being used for both single requests and subscriptions, I also acknowledged what you said here in the original issue:
I tried to argue that this is confusing (the code does not seem like it should work, so it's surprising that it does), and is awkward and brittle to write. It's not at all obvious, using the protocol, that provider non-existence can be detected, and that this how you would do it. Provider non-existence is an important thing for a consumer to know, but the protocol fails to represent this to consumers; the protocol just expects consumers to work it out for themselves. As I said, the proposal doesn't even mention anything about this. Your indifference suggests this is by design; I think, in good faith, that that's a problem. (Edit: it's also the case that, because of what I believe are poor semantics, a consumer that subscribes and wants to check for provider existence is forced to confirm provider existence every time the value is provided, rather than just the first time). As for the topic of consumers discovering providers and subscribing: it's especially discouraging that the one part of what I wrote that you've properly engaged with is the part you wanted to criticise in detail. This is the part where I really might just have no idea what I'm talking about, and I'm happy to have it explained to me why I'm wrong, but I've read the proposal several times, and I can't see how any part of it describes how a provider of subscriptions can dynamically change. The
I have, again, read the proposal several times, and I am, again, happy to have it explained to me how I'm wrong, but it seems to me that is definitely not what the proposal says the semantics of
Like you, I care about the web and want web components to succeed. I'm grateful for the work that has been done and is being done by the WCCG, including on this protocol. |
@mattlucock thanks for sharing your thoughts! Lots of great ideas here that we look forward to getting to the bottom of. Like @benjamind, I am interested in the nuances of this alternative approach but also have apprehension regarding the requirement of
This means you are just in time to join the convo! 😉 It is in use, in production, pretty widely (at least in projects I've related to), so it will be a pretty high bar to see changes, but, as you've noted, your reservations are "serious", so hopefully you'll be able to support the process of kicking the tires here, as it were. I'm still processing all of the OP, but there are at least two main topics that I'd like to address: Re:
|
I have been reading the source of Two notable behaviors in
I've noticed this code example in the current protocol document: class SimpleElement extends HTMLElement {
connectedCallback() {
this.dispatchEvent(
new ContextEvent(
loggerContext,
(value, unsubscribe) => {
// Call the old unsubscribe callback if the unsubscribe call has
// changed. This probably means we have a new provider.
if (unsubscribe !== this.loggerUnsubscribe) {
this.loggerUnsubscribe.?();
}
this.logger = value;
this.loggerUnsubscribe = unsubscribe;
},
true // we want this event multiple times (if the logger changes)
)
);
}
disconnectedCallback() {
this.loggerUnsubscribe?.();
this.loggerUnsubscribe = undefined;
this.logger = undefined;
}
} This example closely follows the 'defensive consumer' example and is described as "A more complete example", but in fact it's completely unrelated to the previous example and introduces new ideas not described anywhere else in the document. The protocol does not describe what it means for a consumer to have a 'new' provider; it doesn't specify that a consumer's provider can change, why it would change, how to detect a change, or how to handle a change. It does not specify that the The protocol does not specify any concept of inter-provider communication, yet such a mechanism is integral to Lit's implementation of the protocol, and doesn't specify any mechanism for by which providers can move subscriptions between each other. The protocol doesn't specify any mechanism for faking or replaying requests. The protocol says that requests are dispatched by consumers and caught by providers. This directly lead me to believe that requests are synchronous, but this is, apparently, completely wrong; I'm assuming that it's wrong, but it's not specified by the protocol. The protocol specifies two kinds of participants: consumers and providers. Lit's I thought this initiative was supposed to be about standardizing the protocol between implementations, but Lit uses what is effectively a custom, non-standard version of the protocol that relies on lots of behavior not specified by the protocol. Again, it seems like this version of the protocol is actually what the protocol is supposed to be, and that the document is missing significant sections. This is so incredibly confusing that this entire issue was essentially borne out of this confusion; it's no wonder my interpretation of the protocol was wrong. |
I've been reading the source of It's disappointing and frustrating that implementations of the protocol have incompatibilities because what I assume is the most dominant implementation has gone ahead and implemented a lot of non-standard behaviour. It seems clear to me that the current specification of the protocol is inadequate and needs significant changes. I see #50, which is good and necessary, but I do disagree with the framing of "registering learnings"; they've just been making significant changes to the protocol that were never included in the specification. I don't really understand how or why this happened. It is disappointing and frustrating that it's been realised that significant changes need to be made to the specification only now after the specification has been given candidate status. Its current candidate status feels erroneous; it seems to me that the specification is very much still a draft. |
Something that might be productive to take into mind before anyone lets frustration or disappointment prevent them from positive action in this area:
Armed with this information, I hope that you or other interested parties will follow up on the requests above so we can continue to explore this area of functionality with data for the benefit of all! |
To be clear, I'm happy that the Lit authors identified shortcomings of the protocol and developed solutions to them, but I'm not happy that those solutions were not incorporated back into the protocol. It defeats the point of this whole exercise if implementers rely on non-standard extensions to the protocol, especially if those extensions break compatibility with the existing protocol as specified. My main frustration and confusion here is that it seems like Lit's extensions to the protocol should have and were meant to have been incorporated, but for some reason were not. @justinfagnani identified a number of shortcomings of my proposed protocol, but in fact, all of those shortcomings currently exist in the protocol as specified. I was so confused by his response. It's true that Lit addresses those shortcomings, but it felt like he thought Lit's version of the protocol actually is the protocol as specified. It seems like the there is a fundamental mismatch between the protocol as currently specified and the protocol as imagined and intended. I was, and still am, very confused by this. As I said above, this whole issue was essentially borne out of this confusion and miscommunication. I don't know the details of how TC39 specifications are developed, but I suppose that was the frame of reference I was using when thinking about these protocols. I'm grateful @Westbrook for you taking the time to talk about these protocols as a concept and what the process for developing them is like. I would suggest that the protocol should be voted down a level, but in saying that I realise we would need to first specify what needs to change and why before we do anything; I guess I'm just thinking ahead. I do think a version of the protocol that effectively incorporates these changes would be substantially different to the current version of the protocol. I hope I've made my perspective clear. I'm grateful for the responses and the patience. I'll close this. |
To be honest I actually like the improvements that were suggested here to the event-driven context. The inversion of the "context-provide" events from providers side back to consumers is really great thing as it will allow to completely eliminate the necessity to store any references about consumers on the provider side whatsoever because providers can use existing I feel like these changes would cover most if not all of my original concerns about the current state of the protocol in respect of it's API design clarity/usability. The last concern that is not yet clear in this scenario is how to handle transfer of the consumers from one provider to another here. I'm a bit lost regarding why this issue was closed as it does not seem to me like we've reached any conclusion - did we dismiss the ideas outlined here or did we like them and what to do next? |
Hi @gund, I appreciate the supportive sentiment. I think the conclusion from the issue was that my proposal as written is undesirable because it doesn't support out-of-order hydration etc, but that the currently-adopted protocol as written is deficient because it also doesn't support out-of-order hydration, so either way we need to make changes to the protocol. The immediate follow-up to this issue was #50, which is yet to be actioned (but I intend to action it soon; I've just been busy). Even if we don't do any of the changes I proposed, I think there are some issues with the current design of Regarding whether this issue is worth incorporating at all: I think I have been persuaded that this is worth caring about. The way I see it, the issue is that the system could easily end up in an invalid state. Let's say A is a parent of B which is a parent of C, and let's say C initially consumes a context from A. Conceptually, a consumer should have the context value from its closest provider, so if B begins providing the context but C does not consume it, C's context value is invalid. I agree that this is an edge case, but I think how this case should be handled probably needs to be specified by the protocol, because if implementers don't handle it in an interoperable manner, this seems to defeat the point of the protocol itself being interoperable. This has already happened, as I mentioned in my previous comment. All of that said, I think there is a way that my proposal can be adjusted to achieve all of the intended benefits of it while also accommodating this case (I closed this issue simply because it was clear that significant reconsideration was required). In |
I see, thanks for elaborating, that makes sense now. Regarding the cost of an extra event being dispatched during the context update to me it seems like a benefit because of the invention of the control of the subscription (consumer controls vs provider controls) and also it is greatly aligned with the web platform itself semantically as we are not reinventing the wheel for simple stuff that already has been solved and provided by the web platform. Also another aspect of a good spec is to give enough room for the implementers to do their job, in a sense that could enable them to make optimizations where it makes sense in their implementation and also possibly provide extra features if they so choose. That is precisely why I actually raised the question about the scope of this spec and maybe we should simply focus on the core functionality where it guarantees only the direct event requests but doesn't guarantee anything more complicated like lazy-context initialization and context transfer between providers. If the spec would be generic enough and support enough decoupling between consumers and providers these features would be really trivial to implement on top of the base spec, just like lit context is doing currently with their "root context" concept. Additionally maybe we can break down this spec into multiple levels of sophistication, like having a base spec which only supports direct and just in time context and then additional features like lazy context and context transfers as additional specs that implementers can choose to implement or ignore. This is essentially how I feel about the spec - to make it as decoupled as possible to free up the implementations to be as open as possible while fully interoperable. But if we really want to go with a fully decoupled consumer/provider then we have to use separate events for context requests and context responses which are dispatched directly to the consumers and not on providers, as this will allow for dynamic provider changes without consumers needing to ever care about it. |
We're definitely in agreement about the events.
Again, I think I disagree with this in this context (pun not intended). Implementers can already do, and have already done, whatever they want. The entire purpose of this exercise is to try and standardize a protocol so that varying implementations can interoperate. If implementers extend the protocol in their own ways, then their implementations aren't interoperable–which isn't the end of the world, but seems to defeat the point of the exercise. As I mentioned, using
I was about to say this sounds exactly like #39, but then I realised that you're author of that issue (it all makes sense now). I generally agree with the sentiment from @justinfagnani in that issue; in fact, I actually think your proposed design is too complex. I understand the general concern about this out-of-order hydration stuff, since it seems like this would be significant source of complexity and 'sophistication' as you put it. Indeed, the current design of |
Just to add a few points on the topic of "more open spec" - I definitely do not mean to have as little in it so it will cause two implementations to not interoperate (like you show example of current state of And this is why I think it's important to make coupling between consumers/providers loose enough so that there is room for different implementations which can still easily interoperate without having to deviate from the spec because it's way too strict on the implementation details. And this is exactly where I think it makes the difference to "open up" the spec to support more sophisticated use-cases like lazy-contexts and context transfers between providers, because when context events are dispatched to the consumers directly - providers are in control and this will allow providers to dynamically change is required and we do not have to specify in the spec how as this very much irrelevant and can be implementation specific but still interoperable because of the way context updates are delivered via events directly to consumers. I'm not sure if I was able to convey my thought process on this fully but I hope there is at least some clarity on the reason why I think dispatching context events on consumers instead of providers gives more freedom to implementations and will allow this spec to focus on core things and leaving specifics to implementations while still guaranteeing interoperability. |
Just in the last few hours or so I've gotten a proof-of-concept implementation of my idea working in, and I think it's quite promising. The entire thing—events, provider, and consumer, all with full support for out-of-order hydration—is ~140 lines. Two things:
We'll circle back on this once I've written up my proposal; I'll be very keen to get your feedback. Thank you for your feedback and comments on this. |
BTW I have a live real project based on |
This issue is fundamentally similar to #39 in that the author of that issue and I both agree that the protocol should be more event-driven, but we have very different ideas of how to go about it. In order to motivate my idea, I need to outline the problems I think it would solve. Most of this is me outlining the problems I think currently exist, and given there could exist alternative solutions to my concerns, I thought it apt to make a separate issue about the concerns and then also propose a solution.
Concerns about the current design
The callback doesn't make sense if the consumer isn't subscribing
If a consumer wants to synchronously request a context value one time, they have to do something like this:
We know, with knowledge of the protocol, that this works, given that a provider will immediately call the callback, but it's actually not obvious from reading the code that this works; it's actually surprising that this works.
I also think it's just awkward. I suggest that the reason it's awkward is because a callback is simply an incorrect representation of the semantics. It's a callback that will be called, synchronously, only once; I don't think it makes sense.
The biggest problem with this, and possibly the biggest problem with the whole design, is that a provider can call a consumer's callback multiple times when the consumer only expects it to be called once. The proposal directly grapples with this, introducing the idea that providers are capable of being 'bad actors' and that consumers can be 'defensive'. I feel strongly that the fact this is even possible is not a necessary trade-off, but a flaw in the design; it doesn't make any sense that this is possible.
Another problem is that it's possible for the provider to cause a memory leak by retaining a consumer's callback when the consumer did not expect it to be retained. The proposal discusses this, of course, but I think it just shouldn't be possible.
The primary advantage I can see of using the callback for this in the current design is that it means the provider provides the value to the consumer in the same way regardless of whether the consumer is subscribing, but I think there's a better solution.
The current approach to unsubscribing is odd
In order to unsubscribe at a time of its choosing, the consumer has to fish the
unsubscribe
function out of the callback in the same way they have to fish the value out of the callback, which is awkward. I, again, think it's awkward because the consumer unsubscribing in this way is not a good representation of the semantics.We want to return values to the consumer, but we also want to return an unsubscribe function to the consumer. The callback seems like a convenient way to do this, but in fact, it overloads the callback with responsibility; providing the value and providing the unsubscribe function are distinct responsibilities.
I think that consumers should unsubscribe using an
AbortSignal
. This would not only be a better representation of the semantics and a better separation of responsibility, but this approach is already in use withEventTarget
and it would be good use of built-in APIs—which, after all, is what web components are about.I note that it would be possible to allow this by having the consumer attach an
AbortSignal
to thecontext-request
event within the constraints of the current design, butsubscribe
flag, the callback, and the signal.As an aside: does the provider pass the unsubscribe function every time they call the callback, or just the first time? The proposal doesn't actually say. If it has to be passed every time, I think that's undesirable for both the provider and the consumer.
How does a consumer know whether there exists a provider?
From what I can tell, if a consumer wants to know whether there exists a provider, the consumer needs to do something like this (which I note is basically the same as the code above):
This is awkward and confusing for the same reasons as above.
The proposal does not address how a consumer can detect whether a provider exists, which, to me, seems extremely important: a consumer requests data but has no idea if they'll ever receive it. Surely the consumer would want to handle the case where they won't.
My idea
I really like the idea of common, community context protocol, and I want this initiative to succeed, but I have serious reservations with it as it is currently specified. Here is my idea for an alternative design:
The provider implements a
getContext(context)
method that returns the value of the specified context in the provider, and also emits acontext-change
event (that doesn't bubble) whenever the value of one of the contexts it provides changes.When a consumer requests a context, they emit a
context-request
event that bubbles up the DOM in order to 'discover' whether there exists a provider for the specified context. If a provider can provide the specified context, it stops the propagation of the event and sets itself as a property on the event (event.provider = this
). This is similar in many ways to the existingcontext-request
event described, but achieves something fundamentally different.After the consumer emits the event, they can retrieve the provider from the event and handle the case where no provider exists. If the provider exists, they can call
provider.getContext(context)
to retrieve the value of the context. That's it. If the consumer wants to subscribe to the context, they can callgetContext()
to get the value of the context at the time of subscription, and then attach acontext-change
listener to the provider to become aware of any future values. My instinct is that event should only specify the context that changed, and not its new value, because this would give the consumer more than one correct way of retrieving the value (getting it from the event or getting it from the provider directly).I think this is an overall simpler design that also addresses the flaws in the current design. The consumer easily detects whether a provider exists, and can retrieve a context once without any unnecessary complexity or pitfalls. If a consumer optionally wants to subscribe to a context, they can do so by attaching event listener, without concerns about how the provider will call the listener, whether the provider will retain it, how they will extract values from the callback, or how they will unsubscribe. I believe that this design would address the problems with the proposed solution in #39 while ultimately addressing the concerns originally raised in the issue. It would also comprehensively solve #21, which I feel still has not been genuinely solved.
Funnily enough, the proposal actually mentions a potential "alternate API" in which consumers emit an event to discover providers, but does not contemplate any reason why this might be advantageous other than type-safety concerns, which the proposal (in my opinion) rightly dismisses. From my perspective, this is a little frustrating.
Trade-offs
One trade-off with this design is that the consumer gains a reference to the provider, thereby gaining at least some knowledge of who the provider is. However, I think this design actually decreases coupling between the provider and consumers. In the current design, providers are forced to be aware of consumers and to deal with their callbacks. In my proposed design, providers have literally no knowledge of consumers. In TypeScript, the type of the provider would be represented in such a way that the consumer could not become coupled to the provider.
Another trade-off is that providers would need to specify in their
context-change
events what context changed, and consumers would need to inspect the event to verify that it's actually relevant to them. In the general case, just as mostcontext-request
events are not relevant to any given provider, mostcontext-change
events would not be relevant to any given consumer.Example TypeScript
I've taken some liberties in how I've written this that aren't necessarily important. One issue I have with the code in the proposal is that
createContext
is a JavaScript function that exists only for TypeScript purposes; it returns a nice generic type, but it doesn't actually do anything. TheContext
type in the proposal requires us to specify the type of the key even though we don't actually care what its type is; we need to specify the type of the key just so that the key is assignable to the type of the context e.g."my-context as Context<string, number>
. If we were to restrict the type of keys ahead-of-time, even with a very loose restriction likestring | symbol | object
, specifying the type of the key becomes unnecessary e.g."my-context" as ContextKey<number>
.Closing thoughts
I don't know whether any of this alternate design is actually of any use. Given that the proposal is now candidate status—as of, apparently, two weeks ago—it probably can't be redesigned in a meaningful way at this point, which is disappointing. I've only just discovered the WCCG and this proposal recently; if I had found this earlier, I would have participated earlier.
Regardless, I think the issues I've described are of real concern, and I would be keen to try and address these issues in any way that is feasible.
I'm very keen to hear any thoughts anyone has.
The text was updated successfully, but these errors were encountered: