-
Notifications
You must be signed in to change notification settings - Fork 390
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
Maintain backwards compatibility with wire format from 4.3.0 #629
base: main
Are you sure you want to change the base?
Conversation
This would enable communication between 4.3.0 and the new latest. But latest communicating with 4.3.1/4.4.0/4.4.1 would not work. Wouldn't keeping the strings, and adding an additional mapping for the integers be better? Since it would then support all versions, at least with one way communication. |
@MrMadClown Very good point! So to summarize your proposal, we would allow this type of communication: // 4.3.0 ------------------> latest
const a = Comlink.wrap(port) Comlink.expose(stuff, port)
// For instance this
a.foo()
// Will trigger:
postMessage({ id: '<uuid>', type: 2 /* APPLY */, ... })
// The other end, notice that type is an integer, maps it to a string (or just go through the switch case)
// and make sure to return a wire value using an integer instead of using a string, so:
postMessage({ id, type: 0 /* RAW */, value })
// Need to do something for:
a.bar(Comlink.proxy(() => console.log('hello world !')); All good. And this wouldn't work: // 4.3.0 <----------------- latest
Comlink.expose(foo, port) const b = Comlink.wrap(port)
// For instance this
b.foo()
// Will trigger:
postMessage({ id: '<uuid>', type: "APPLY", ... })
// "APPLY" does not match any possible values, so it break Communication with 4.3.1, 4.4.0 and 4.4.1 would be unmodified. @d3lm I think this should be enough for our us. Will update this PR with a proposed solution. |
The approach taken is a bit different from the previous commit. We now support type values to be either the format from 4.3.0 or the format introduced in 4.3.1. When communicating with 4.3.0, the only way to communicate is if it's done like this: // 4.3.0 ------------> latest const a = Comlink.wrap(ep) Comlink.expose(b, ep) This works because `a` has to initiate the conversation with `b` giving us a chance to always treat future messages as 4.3.0 ones. In particular we support proxy sent by `a`: a.registerCallback(Comlink.proxy(() => {})) But we also support sending MessagePort that can be later wrapped by the other side: a.callMeBack(Comlink.transfer(port, [port]))
tests/fixtures/v430comlink.html
Outdated
callme(port) { | ||
Comlink.wrap(port).maybe(); | ||
} |
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.
Note that this code does not care whether instances of Test
are exposed to an endpoint that need to talk 4.3.0 format or the latest format.
The trick is that when port
is being received here, we've "marked" it so that if any code path tries to wrap
it, the code will automatically work.
If that trick does not work because the port is nested inside an object, you can always resort to passing Comlink.wrap(port, undefined, true)
manually which would force the proxy created to use the 4.3.0
format.
Would it make sense to call the |
export function wrap<T>( | ||
ep: Endpoint, | ||
target?: any, | ||
legacy: boolean = false |
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.
This is provided for cases where prior knowledge has not been obtained. For instance if instead of doing this:
a.foo(Comlink.transfer(port, [port])
The caller does:
a.foo(Comlink.transfer({ prop: port }, [port]))
In this case if inside foo we do:
foo({ prop: port }) {
const remote = Comlink.wrap(port);
// Oopsie this will break! It won't use the legacy format.
remote.oopsie();
}
In this scenario because we don't traverse the object { prop: port }
we don't mark port
as legacy. We could traverse them but this might have a huge overhead and so I opted for not doing it.
Instead the implementer can do:
foo({ prop: port }) {
const remote = Comlink.wrap(port, undefined, true);
// Ok fine this works :)
remote.oopsie();
}
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.
If we want to add the traversal then only markLegacyPort
needs to be updated
With traversal the communication with an older version of the library will always work as long as the code using the older version of the lib is the one to initiate the communication. Also the overhead is probably not as bad as I thought given this: 1. Does not impact the new format 2. Does not make it worse for function with many arguments 3. Only has a big impact on trees being transmitted. However doing structured cloning on a tree is already costly and probably be something that should be avoided in the first place.
Is there a chance we can move forward with this PR and get a review on this? It'd be really nice if we could maintain backwards compatibility to some degree. |
I don’t mean to nag, but it'd be really nice if we could move forward with this somehow because it's pretty important for us. Is there anything else we can provide to get this moving? |
I don't have much time right now to look deeply into this I'm afraid. I am hesitant to move ahead with legacy protocol support without some plan in place for when legacy support will be removed. It's quite a bit of complication and maintaining that going forward seems more hassle than it's worth for a library that isn't in major active development. @surma your thoughts? |
My understanding from using this library is that the wire protocol is an internal implementation detail and not something that's part of the public interface. |
@defunctzombie It might seems like it because it isn't exposed in the public TypeScript API of this library. However the endpoint used (whether it's a For instance you could have two esm modules that could talk to another as long as they use the same wire format. One of them could explicitely use The goal for this PR was to partially revert a breaking change in the wire format that was introduced as part of the If you want to convince yourself, here's a playground: |
Sure - but I'm wondering if thats outside the goals of the project. This is moving into the realm of things like protobuf and similar. I am not speaking for the project maintainers - only as a user myself. I had imagined it was more focused on providing a nicer RPC interface for the workers it spawns - which also come from the same release of the application. I can see your point though that if you assemble an application from a few components, two components might use different versions of comlink and the ask is that within a major series they are compatible with each other. That's a reasonable ask if the maintainers consider that the wire interface is something that is semi-public in this manner. It would be equally reasonable to say only the JS/TS interface is public and everything else is internal implementation details that work only with the exact same version talking to the same version. We can see what the maintainers say about their initial intent with any wire interface. |
Hi 👋
This PR adds backwards compatibility with the wire format from
4.3.0
(or lower). This is something that we need at StackBlitz in order to keep using the upstream version ofcomlink
(which we would love to! ❤️ ).Why?
Both
Message
s andWireValue
s have atype
property. Untilcomlink@4.3.0
those values used to overlap. In version4.3.1
, those values were changed tostring
s.At StackBlitz, we have limited control over one kind of apps (which might be forever locked with
comlink@4.3.0
) that consume ourcomlink
interface. When they consume this interface they initiate the communication by doing aComlink.wrap
eventually followed by method calls, sending clone-able data orMessagePort
s. ThoseMessagePort
s are eventuallyComlink.wrap
ed on the other end.This PR propose changes to make those communication use cases working with at least
4.3.0
(maybe other older versions might work as well but it hasn't been tested and I haven't added test for them in this PR).How?
Let's look at an example. Supposing we have
foo.com
that does:And on
bar.com
, an API is exposed:When we then do on
foo.com
:This will essentially do this:
Proxy (and more generally MessagePorts) are supported too:
For proxy the flow is similar but we "mark" port as using the legacy format:
EDIT: Updated description to reflect the current state of the PR (see edit history to see the description matching the earlier commits)