Skip to content
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

DevTools <-> VSCode integration #1497

Merged
merged 19 commits into from
Sep 20, 2024
Merged

DevTools <-> VSCode integration #1497

merged 19 commits into from
Sep 20, 2024

Conversation

phryneas
Copy link
Member

@phryneas phryneas commented Aug 29, 2024

This is in very early stages and split up between here and apollographql/vscode-graphql#188

@phryneas
Copy link
Member Author

And the Explorer tab is working 🎉

image

@phryneas phryneas marked this pull request as ready for review September 4, 2024 14:40
@phryneas phryneas requested a review from a team as a code owner September 4, 2024 14:40
@phryneas
Copy link
Member Author

phryneas commented Sep 4, 2024

I'll fork off here into additional features - this is a good size for a review already :)

@jerelmiller jerelmiller changed the title [WIP] DevTools <-> VSCode integration DevTools <-> VSCode integration Sep 12, 2024
Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have more with fresh eyes, but I think this is a good set of feedback to take a look at for now. I'd definitely recommend seeing if you can reduce to a single rpc client/message adapter in the server if possible so you can take advantage of the message bridge.

@@ -0,0 +1,36 @@
{
"name": "@apollo/client-devtools-vscode",
"version": "4.18.4-pre.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this match the initial version in package.json? Looks like that is on 4.18.6 if you want to update the patch version here to keep it in sync (though not sure how much it matters yet at this stage or not)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was just for my local build back when I forked off - when we publish the package, it will go through changesets and inherit the version of the extension :)

package.vscode.json Outdated Show resolved Hide resolved
src/extension/vscode/client.ts Outdated Show resolved Hide resolved
src/extension/vscode/client.ts Show resolved Hide resolved
src/extension/vscode/polyfills.ts Show resolved Hide resolved
};
}

function createWsAdapter(ws: WebSocket, signal: AbortSignal): MessageAdapter {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to do a bit of organization on the existing code so that this is near the other code, but ok for now and we can refactor in a future PR.

src/extension/vscode/server.ts Outdated Show resolved Hide resolved
Comment on lines 117 to 119
connected: new Promise<void>((resolve) =>
ws.addEventListener("open", () => resolve(), { once: true, signal })
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of this name mostly because reading this I keep expecting this to be a boolean, but its a promise instead.

Out of curiosity, could we just use a callback here and call this onConnected? We can remove the promise altogether then.

Suggested change
connected: new Promise<void>((resolve) =>
ws.addEventListener("open", () => resolve(), { once: true, signal })
),
onConnected: (callback: () => void) => {
ws.addEventListener("open", () => callback(), { once: true, signal })
},

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would miss the event if you added an onConnected event after the connection is already established and handling that would add extra logic. I'm open to changing the name, but I'd like to keep the promise, as that handles that nicely.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a good point. Let's see if we can rename this then.

Would it be out of the question for registerClient (or whatever the new name will be) to return a promise instead? If not, my brain is out of names 😆

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connectedPromise - will at least not be confused with a boolean, and also rejects now when there is a connection error

Comment on lines 120 to 121
unregister: cleanup,
onCleanup: registerCleanup,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
unregister: cleanup,
onCleanup: registerCleanup,
disconnect: cleanup,
onDisconnect: registerCleanup,

What about calling this disconnect? I think this will probably be a bit more intuitive than "cleanup" since its not immediately clear what is being cleaned up without knowing the implementation details. Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's called unregister right now, because the function itself is named registerClient.

We could rename registerClient to connectClientToDevTools or something, then disconnect would make sense 🤔
Not sure if I like that better, though - WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I do like that better! I had a similar thought this morning before reading your comment so I'm on board. A name like connectClienttoDevTools makes it immediately obvious what that block of code does without having to follow the import to understand what you're registering your client with. On top of that, it better follows with our connectToDevTools option naming we've had in Apollo Client for for a while so theres some symmetry there 🙂

Copy link
Member Author

@phryneas phryneas Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I'd suggest we make it really obvious with a connectApolloClientToVSCodeDevTools? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha either one, but something with connect and devTools in it makes sense to me :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to connectApolloClientToVSCodeDevTools and also edited the shape of the returned object

const wsRpcHandler = createRpcHandler(wsAdapter);
const wsActor = createActor(wsAdapter);
wsActor.on("registerClient", ({ payload }) => {
payload.id = id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok I see this is set here instead of in client.ts, which I missed when adding the other comment.

I'm curious why you decided to go this route of creating a handler/rpc client per apollo client instance? If you're able to use a single client/rpc handler, you would then be able to take advantage of the message bridge since this script would essentially just be the middle man between the client script and devtools.

I'd recommend taking a stab at that, otherwise we'll need to edit this file anytime we want to add a new message from devtools to the client (such as the memory panel that will be added soon). I used to do this in tab.ts and it there were definitely times that I scratched my head wondering why something wasn't working only to realize I forgot to add the message forwarding between the two. The message bridge definitely helps out since its a bit more automatic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message bridge would work if we had the guarantee of a single connection, but you can totally connect two different processes to one server (e.g. connect an iOS emulator and an Android emulator at the same time). So either at that point I send everything everywhere, or I have to keep track of who is who in the server - I went for the latter.

Also, we could in the future add some "snapshot" functionality here inside the server that would work by creating a "virtual client" that would be fully controlled by the server.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right that makes sense.

I suppose the thing that worries me is diverging some of the client-specific behavior too much from each other between the browser extension and the vscode integration. It makes sense that we have different transport mechanisms (websockets vs ports) and have varying code there, but if we can make some of the core client logic the same, I think it will be easier to maintain long-term.

In this case, I'm specifically referring to where/how the client id is created and assigned. In the browser extension, this is created when registering the client with the browser extension. Here it is created and assigned in the server instead. If we end up diverging too much on this kind of thing, I fear it will get difficult to maintain as it means some of the common abstractions may only work in one of two contexts.

I'm not opposed to assigning it in the server, but if we go that route, I'd love for the browser extension to resemble this structure as well. It wasn't immediately obvious to me id was going to be assigned outside of registerClient in the code when reading through this the first time (hence my barrage of comments).

For now though, what do you think about at least creating the id over in registerClient and just reading that here instead of creating/assigning it in the server?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating the id inregisterClient now, I just hope there won't be any collisions in low-entropy environments

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that I'm that worried to be honest. At most we'll have maybe 10s of clients connected at once, at least in the 99% of cases. If we get reports of collisions between connected clients because we start getting tons of them, we can always revisit.

Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Thanks for making those changes 💯

const wsRpcHandler = createRpcHandler(wsAdapter);
const wsActor = createActor(wsAdapter);
wsActor.on("registerClient", ({ payload }) => {
payload.id = id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that I'm that worried to be honest. At most we'll have maybe 10s of clients connected at once, at least in the 99% of cases. If we get reports of collisions between connected clients because we start getting tons of them, we can always revisit.

@phryneas phryneas merged commit 9aabb7a into main Sep 20, 2024
7 checks passed
@phryneas phryneas deleted the pr/vscode branch September 20, 2024 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants