-
Notifications
You must be signed in to change notification settings - Fork 5
refactor: consolidate API logic into CoderClient class #125
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
Conversation
…ent with base function
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.
Fun to see it come together!
this.trackedEjectionIds.delete(ejectionId); | ||
} | ||
|
||
return sizeBeforeRemoval !== this.trackedEjectionIds.size; |
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.
Very much a nit haha, but why a size check over return true
in the if
otherwise return false
?
Another nit, since this is only used in one spot, is it worth a method? Could maybe just have eject
there and not add/remove to the tracked IDs at all, since the finally
will always remove it anyway.
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.
Yeah, good call on the size check – your suggestion's a lot more straightforward
As for whether the method makes sense, I guess my gut feeling is that if I'm going to have a special add method for the interceptors, it makes sense to have an obvious way to remove them, too. Plus, since it's only a private method, it'd be easy to remove down the line, too
// Manually aborting a request is always treated as an error, even if we | ||
// 100% expect it. Just scrub the error if it's from the cleanup | ||
if (errorIsFromCleanup) { | ||
return undefined; | ||
} |
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.
Will this cause calls not to error? Like if I was using this in my own project, and called client.getWorkspaces()
and it got aborted, would it return with undefined
?
If so, part of me wants a more explicit Go-like syntax to check if the error is an abort error (errors.Is(err, context.Canceled)
) but at the same time, maybe this is React-y, and as long as the editor tells me "undefined means aborted" when I look at the function definition, that would not be too unreasonable.
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.
So, as far as I can tell from the Axios docs, the way the error interceptor works is that, Axios will run all the interceptors, and then if the value it gets back is not undefined
, Axios will treat that value as an error and throw it (even if the value isn't an Error
instance). I don't think it's inherently React-ish – it's strictly an Axios thing
I could've just had return
, but Backstage's linters complain about mixing return
and return <value>
in the same function
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.
Ah yup, I get that the return undefined
here is causing Axios to not throw and not being used itself as the return of client.getWorkspaces()
, but I wonder what now is the return of client.getWorkspaces()
with the error having been scrubbed? Since it did not throw, but there was also no data because it aborted, is it just naturally undefined
maybe?
And if so, maybe returning undefined for canceled things is React-like so this is fine, was my thinking, but honestly I have no idea. 😛 So you would write code like:
const workspaces = client.getWorkspaces()
if (workspaces === undefined) {
// This means the request was canceled
}
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.
Ooh, yeah, good point – realistically, I only expect the cleanupClient
function to be used for testing, but that is a hole in the logic that could pause pain points down the line
It's not the worst thing in the world – React Query exposes the error
property as type unknown
by default to be defensive, so technically, all RQ code already has to deal with an "error" potentially being undefined. But if I do need to keep this method for connecting to the Coder core files, I'll be sure to figure something out
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.
Oh yeah good point, this really only comes into play when cleaning up the client.
// The Axios docs have incredibly confusing wording about how multiple | ||
// interceptors work. They say the interceptors are "run in the order | ||
// added", implying that the first interceptor you add will always run | ||
// first. That is not true - they're run in reverse order, so the newer |
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.
Yikes that is confusing, good catch!
* @file This is a temporary (and significantly limited) implementation of the | ||
* "Coder SDK" that will eventually be imported from Coder core |
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.
I like this approach!
const originalGetWorkspaces = baseSdk.getWorkspaces; | ||
baseSdk.getWorkspaces = async request => { |
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.
Is this override temporary or here to stay? I wonder if we override it with the return below instead, like:
const getWorkspaces = () {}
// stuff
{
...baseSdk,
getWorkspaces,
}
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.
Yeah, good call. I think doing it that way is more clear
const templateIconUrl = ws.template_icon; | ||
if (!templateIconUrl.startsWith('/')) { | ||
return ws; | ||
} | ||
|
||
return { | ||
...ws, | ||
template_icon: `${assetsRoute}${templateIconUrl}`, | ||
}; |
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.
Mostly just musing, but if we did not have a proxy we could use URL
to resolve the base:
const templateIconUrl = ws.template_icon; | |
if (!templateIconUrl.startsWith('/')) { | |
return ws; | |
} | |
return { | |
...ws, | |
template_icon: `${assetsRoute}${templateIconUrl}`, | |
}; | |
return { | |
...ws, | |
template_icon: ws.template_icon ? new URL(ws.template_icon, deploymentURL).href : undefined, | |
} |
const tokenIsInvalid = | ||
err instanceof AxiosError && err.response?.status === 401; |
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.
To piggy back off my comment in the other PR, we should probably validate the response data.
Does AxiosError
cover all non-20x status codes? I thought maybe it only covered failures to make the request.
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.
Yeah, agreed – going to fix really quick
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.
Actually, I looked this up, and the answer is "it depends". By default, AxiosError
does cover all non-2xx status codes. But you can add a validateStatus
function to the axios.get
call to customize your logic for how you treat the status code
So right now, CoderClient
isn't doing that, and neither is Coder Core. But once we bring in the core files, things could always change if we're not careful
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.
Oooooooooooooohhh right now that you say that, I remember seeing validateStatus
in Coder core. That makes sense.
// Actual request type doesn't matter; just need to make some kind of | ||
// dummy request. Should favor requests that all users have access to and | ||
// that don't require request bodies | ||
await this.sdk.getUserLoginType(); |
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.
getUserLoginType
does seem reasonable to me, but in our other plugins we get the user; maybe it could be unclear why we do it differently in one spot? Could be cool to cache that user info at some point too in case we want to show the user's name or something.
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.
Oh, for some reason, when I was looking at the functions in the api.ts
file, I somehow missed getAuthenticatedUser
I wanted to mirror the function calls that we have there, but if we still have this function, I definitely think it makes more sense to use that
} | ||
}; | ||
|
||
cleanupClient = (): void => { |
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.
I like this how thorough this is with the cleanup logic, but the client is meant to last for the lifetime of the application, right? Is there a case where you would need to recreate the client? I wonder if we should write it with the assumption it is long-lived, at least until/unless there is a use case.
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.
Yeah, it's sort of a carry over from the previous PR I closed out. There, it actually was necessary to have cleanup logic, because the file was operating under the assumption that we'd be importing a single Axios instance from the Coder core files, and you needed a way to ensure isolation for Client instances spun up for different test cases. Right now, CoderClient
gets a brand new Axios instance each time it's instantiated, but that may or may not have to change, based on what I can change in core
So at least for right now, I kind of threw it in here just because. The actual CoderClient
in production should basically be a singleton that never gets removed (especially since it's not tied to React rendering, so it can't get conditionally unmounted). Just to be on the cautious side, I'm going to hold onto the method for this PR, and then depending on how much luck I have with the Coder core changes to improve its test isolation, I'll either:
- Remove this method because I'll be able to get guaranteed isolation from Coder core
- Beef up the method because getting isolation set up in Core is harder than I thought
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.
Perfection!
|
||
for (const agent of resource.agents) { | ||
const status = agent.status; | ||
if (!uniqueStatuses.includes(status)) { |
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.
Might be a cool use case for Set
.
Closes #107 (at least until we need to figure out a good shared auth solution)
Changes made
CoderClient
classCoderSdk
class that theCoderClient
relies on, so that it will be easier to bring in the real Coder SDK down the line. The thinking is that it will be a matter of swapping the mock for the real thinguseCoderSdk
hook