-
Notifications
You must be signed in to change notification settings - Fork 945
Add typings for WebChannel #2385
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
1cce950
to
9f7b146
Compare
9f7b146
to
a864185
Compare
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 think these types are diverging from our (unfortunate) reality. 😄
body?: ArrayBufferView | Blob | string | null, | ||
headers?: Headers, | ||
timeoutInterval?: number | ||
): Promise<XhrIo>; |
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.
Why does this return a Promise? Looks like void to me?
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.
My initial attempt to the types from Google3 is the source of this:
This also explains retroactively why the input types are limited to ArrayBufferView | Blob | string
.
send( | ||
url: string, | ||
method?: string, | ||
body?: ArrayBufferView | Blob | string | null, |
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 don't care too much, but this doesn't exactly match the closure types (which include ArrayBuffer
, Document
, FormData
, etc.) or our own usage (where we just pass string
I assume).
If somebody ever needs to modify this in the future, it may not be clear how we ended up with the existing type definition and so it may not be clear how to properly update it.
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.
My new goal is to only support the usage in Firestore. As such, I changed it to just support string.
CLOSE = 'close', | ||
ERROR = 'error', | ||
MESSAGE = 'message', | ||
COMPLETE = 'complete' |
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.
complete doesn't seem to exist on webchannel or be used by our code?
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.
Naive me thought it was possible to re-use the same EventType typings in two different places. I have since gone back on this approach and embraced reality.
} | ||
|
||
export enum ErrorCode { | ||
NO_ERROR = 0, |
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.
Again, this doesn't match reality (where we only export NO_ERROR, TIMEOUT, and HTTP_ERROR).
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 - I removed all not exported members. As part of this I also saw that we export getResponseText
but don't use it. I removed it from the closure wrapper.
|
||
getResponseText(): string; | ||
|
||
getResponseJson(): Object | any; |
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.
nit: is blah | any
better / different than just any
?
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.
Nope. But my new way of returning WebChannelError might be (which I think is needed since we access error.code
and error.message
.
CLOSE = 'close', | ||
ERROR = 'error', | ||
MESSAGE = 'message', | ||
COMPLETE = 'complete' |
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.
Hrm... I don't think this is right. Our webchannel-wrapper module actually only exports EventType.COMPLETE
and the value should come from closure-library (goog.net.EventType.COMPLETE
) rather than us redefine the value to a constant ('complete'
) here. See:
goog.net.EventType['COMPLETE'] = goog.net.EventType.COMPLETE; |
'EventType': goog.net.EventType, |
With your change, this .d.ts file will now let us use e.g. EventType.OPEN
in our code but at runtime it will be undefined
because the actual implementation (in webchannel-wrapper/index.js) doesn't actually define it.
So I think we need to reduce this to only what we export from index.js... and I don't think we should be assigning values ('open'
etc.) to the enum values. In fact, maybe we shouldn't use an enum at all, to make it clearer that the values aren't known (to TypeScript) at compile-time. Poking around, I'm not sure what the right way to do that is, but this seems to work:
export interface EventType { }
export var EventType: {
COMPLETE: EventType
};
I wonder if there's some better way to go about all of this. webchannel-wrapper is an ugly wart to allow us to use (a very small subset of) closure-library from our TypeScript code. The fact that https://github.com/fivetran/typescript-closure-tools exists suggests that maybe there's a better way and we wouldn't have to manually re-expose the part of closure-library we need? Perhaps a good hackweek project! 😛
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 should have done more research prior to this PR, but I think at least now I have gained a rough understanding of how the exports work. I traced them through to the minified code and replaced all types with strings (or numbers for ErrorCode).
@@ -31,7 +82,9 @@ export interface WebChannelOptions { | |||
messageHeaders?: StringMap; | |||
initMessageHeaders?: StringMap; |
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.
Are these usages of StringMap safe for some reason? I'm not clear why you had to change messageUrlParams
and internalChannelParams
but not these.
That said, it would be nice if you didn't have to change any of these and our minification logic was able to handle StringMap
. Is that not possible?
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.
They are only used as empty objects in Firestore. I ended up removing the string map altogether and changed the types to {}
. I hope that may act as more of a deterrent.
close(): void; | ||
halfClose(): void; | ||
listen<T>(type: EventType, cb: (param: T) => void): void; | ||
send<T>(msg: T): 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.
nit: Is there a benefit to making these generic instead of typing them as unknown
or similar?
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.
Send
can be unknown (it should probably not be - but then we should both the generic type on WebChannel). I made it unknown for now.
The in listen
calls back to us so it cannot be unknown.
body?: ArrayBufferView | Blob | string | null, | ||
headers?: Headers, | ||
timeoutInterval?: number | ||
): Promise<XhrIo>; |
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.
My initial attempt to the types from Google3 is the source of this:
This also explains retroactively why the input types are limited to ArrayBufferView | Blob | string
.
CLOSE = 'close', | ||
ERROR = 'error', | ||
MESSAGE = 'message', | ||
COMPLETE = 'complete' |
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.
Naive me thought it was possible to re-use the same EventType typings in two different places. I have since gone back on this approach and embraced reality.
CLOSE = 'close', | ||
ERROR = 'error', | ||
MESSAGE = 'message', | ||
COMPLETE = 'complete' |
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 should have done more research prior to this PR, but I think at least now I have gained a rough understanding of how the exports work. I traced them through to the minified code and replaced all types with strings (or numbers for ErrorCode).
} | ||
|
||
export enum ErrorCode { | ||
NO_ERROR = 0, |
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 - I removed all not exported members. As part of this I also saw that we export getResponseText
but don't use it. I removed it from the closure wrapper.
send( | ||
url: string, | ||
method?: string, | ||
body?: ArrayBufferView | Blob | string | null, |
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.
My new goal is to only support the usage in Firestore. As such, I changed it to just support string.
|
||
getResponseText(): string; | ||
|
||
getResponseJson(): Object | any; |
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.
Nope. But my new way of returning WebChannelError might be (which I think is needed since we access error.code
and error.message
.
@@ -31,7 +82,9 @@ export interface WebChannelOptions { | |||
messageHeaders?: StringMap; | |||
initMessageHeaders?: StringMap; |
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.
They are only used as empty objects in Firestore. I ended up removing the string map altogether and changed the types to {}
. I hope that may act as more of a deterrent.
close(): void; | ||
halfClose(): void; | ||
listen<T>(type: EventType, cb: (param: T) => void): void; | ||
send<T>(msg: T): 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.
Send
can be unknown (it should probably not be - but then we should both the generic type on WebChannel). I made it unknown for now.
The in listen
calls back to us so it cannot be unknown.
@@ -68,8 +68,6 @@ goog.net.XhrIo.prototype['getLastError'] = | |||
goog.net.XhrIo.prototype['getLastErrorCode'] = | |||
goog.net.XhrIo.prototype.getLastErrorCode; | |||
goog.net.XhrIo.prototype['getStatus'] = goog.net.XhrIo.prototype.getStatus; | |||
goog.net.XhrIo.prototype['getStatusText'] = | |||
goog.net.XhrIo.prototype.getStatusText; |
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 not used by our code. Removing it is technically a breaking change though for consumers of this package. We might be ok though: https://github.com/search?l=JavaScript&q=%22%40firebase%2Fwebchannel-wrapper%22&type=Code
@@ -371,10 +369,9 @@ export class WebChannelConnection implements Connection { | |||
// compatible with the bug we need to check either condition. The latter | |||
// can be removed once the fix has been rolled out. | |||
// Use any because msgData.error is not typed. | |||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
const msgDataAsAny: any = msgData; | |||
const msgDataAsAny: WebChannelError | object = msgData; |
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.
The name msgDataAsAny
did not survive your refactor very well. msgDataOrError
?
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.
Sounds good. Renamed.
}; | ||
|
||
export interface Headers { | ||
[name: string]: string | number; |
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.
Out of curiosity, why doesn't this cause problems like StringMap did?
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 opted not to enable renaming for Object literal access. Since all headers are invalid JavaScript identifiers, they are only ever set using the literal access notation (such as header['X-Google-Foo'] = 42
).
messageHeaders?: StringMap; | ||
initMessageHeaders?: StringMap; | ||
messageHeaders?: {}; | ||
initMessageHeaders?: {}; |
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 your intention to require that we set these to an empty object? Because I don't think that's what this actually does. This will allow us to pass any object (e.g. {'foo': 'bar'}
since TypeScript does structural typing (aka duck typing).
Not sure it's possible to specify a type that indicates that only an empty object is valid.
That said, in general this is feeling somewhat fragile which is a bit worrisome since webchannel is already a bit fragile. It wouldn't be too surprising if we tweak some webchannel setting for performance/reliability reasons and then it gets minified wrong because we don't properly update this d.ts file and therefore it's ignored, and we don't notice because it's not something we can readily test.
If that's the world we're heading towards, I'd like to discuss if there are ways to avoid it... and if not, then we will perhaps eventually (once your minification stuff lands) want some big warnings in this file 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.
To be perfectly honest, that was my intention. I did realize this doesn't work but I thought that it might still convey some of the original intent.
But you can ignore all this because I came up with a way that actually works. I can disallow all properties with:
messageHeaders: { [k : string] : never }
Furthermore, all members that have at least a single property specified do not allow any extra properties as is.
export interface WebChannel { | ||
open(): void; | ||
close(): void; | ||
listen<T>(type: string, cb: (param: T) => void): 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 still don't really understand this usage of generics. Is there even any practical difference between:
function foo(x: any)
// and
function foo<T>(x: T)
It seems like it's a way of telling TypeScript to let you pass anything. The generic way looks more type-safe, but I don't think it is?
It seems like the correct type here is unknown
and we should explicitly cast to something else if we need to.
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.
No - there is no difference, but one looks safer! I changed this to unknown
and modified the consumer.
|
||
getResponseJson(): WebChannelError | object; | ||
|
||
listenOnce<T>(type: string, cb: (param: T) => void): 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.
same feedback about generics here.
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.
Removed. I am "casting" in the callsite now.
@@ -310,7 +308,7 @@ export class WebChannelConnection implements Connection { | |||
// Note that eventually this function could go away if we are confident | |||
// enough the code is exception free. | |||
const unguardedEventListen = <T>( | |||
type: WebChannel.EventType, | |||
type: string, |
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.
Was it not possible to make WebChannel.EventType work? It seems like we're losing a bit of help from the type system here by switching to just string
. Not the end of the world though.
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.
WebChannel.EventType is now a variable declaration and no longer a string. I had tried both WebChannel.EventType
and typepof WebChannel.EventType
.
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.
Bleh... I came up with several ways to get this to compile (let us use EventType
as a type as well as hang property values off it). E.g.:
export interface EventType {}
export var EventType: {
readonly COMPLETE: EventType
}
or:
export class EventType {
static readonly COMPLETE: EventType;
}
but they didn't provide type-safety, since TypeScript's structural typing would let you pass anything (e.g. {x: 1}
) for a parameter typed as EventType
. 😕
So the only thing I found that actually works for type-safety is using export enum EventType { COMPLETE }
like you originally had. So if you feel like putting that back, I would be okay with that. Else if you're tired of messing with it, this is okay too.
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.
Hm. My "I couldn't get it to work" response above is based on the current version that used the variable declaration w/ nested property syntax. I am inclined to leave that as is for now, as it matches the underlying library closer than the Enum values.
…s-sdk into mrschmidt/webchannel
@@ -310,7 +308,7 @@ export class WebChannelConnection implements Connection { | |||
// Note that eventually this function could go away if we are confident | |||
// enough the code is exception free. | |||
const unguardedEventListen = <T>( | |||
type: WebChannel.EventType, | |||
type: string, |
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.
Bleh... I came up with several ways to get this to compile (let us use EventType
as a type as well as hang property values off it). E.g.:
export interface EventType {}
export var EventType: {
readonly COMPLETE: EventType
}
or:
export class EventType {
static readonly COMPLETE: EventType;
}
but they didn't provide type-safety, since TypeScript's structural typing would let you pass anything (e.g. {x: 1}
) for a parameter typed as EventType
. 😕
So the only thing I found that actually works for type-safety is using export enum EventType { COMPLETE }
like you originally had. So if you feel like putting that back, I would be okay with that. Else if you're tired of messing with it, this is okay too.
With inspiration from
https://github.com/fivetran/typescript-closure-tools/blob/master/index/closure-library/closure/goog/net/errorcode.d.ts
and https://github.com/fivetran/typescript-closure-tools/blob/master/index/closure-library/closure/goog/net/xhrio.d.ts