-
Notifications
You must be signed in to change notification settings - Fork 928
Revive Node.JS Support for Cloud Firestore (fixes #221). #319
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
* Fix miscellaneous node / GRPC bit rot. * Re-add grpc / protobufjs dependencies. * Remove crazy grpc stream error handling nonsense that is no longer necessary after grpc/grpc#9101 * Clean up grpc_connection logging (and consistently use util.inspect()). * Fix WebChannel / GRPC Connection compatibility issues. * Add an explicit mapping from "RPC name" (e.g. "BatchGetDocuments") to the REST url path (e.g. "batchGet") for WebChannel, and for GRPC just assume the first letter should be lowercased (e.g. "batchGetDocuments"). * Split Connection.invoke() into invokeRPC() and invokeStreamingRPC(), with the latter accepting a stream of results and aggregating them into an array (needed to support BatchGetDocuments RPC). * Fix serializer issues * Query limits are an 'Int32Value' but we were serializing them as a normal int which GRPC / protobufjs didn't like. * Several proto "oneof tags" were outdated. * Add build steps to copy protos into npm package. * Run integration tests for Node.JS * Added to 'test:node' script in package.json and in .vscode/launch.json * Include index.ts for browser and index.node.ts for node so the appropriate PlatformSupport gets registered. * Misc cleanup * Remove unused MockPlatform since we use the normal NodePlatform now. * Remove 'google-auth-library' CredentialsProvider that we used to use for node.js (before we were integrated with FirebaseAuth). * Fixed several tests that were hitting node.js warnings about unhandled promise failures. * mocha commmand-line args: * "--compilers ts:ts-node/register" was deprecated in favor of "--require ts-node/register" * Remove "--retries 5" when running mocha tests from VS Code. * Consistently use "--require" instead of "-r" * Add "--exit" when running from VS 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.
:) I'm so happy.
Supposedly WebStorm EAP will allow us to run tests from the IDE. I'm still running into errors, but determined to make this work once this is merged.
packages/firestore/package.json
Outdated
@@ -18,7 +18,9 @@ | |||
"module": "dist/esm/index.js", | |||
"license": "Apache-2.0", | |||
"dependencies": { | |||
"@firebase/webchannel-wrapper": "^0.2.4" | |||
"@firebase/webchannel-wrapper": "^0.2.4", | |||
"grpc": "^1.6.6", |
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.
Did you try to run these tests with the latest versions of these dependencies?
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 have now... It turns out grpc now has typings defined so we could remove our hacky ones (I also had to tweak a few things to make our code compatible with the defined typings).
@@ -339,6 +333,8 @@ export class WebChannelConnection implements Connection { | |||
|
|||
// visible for testing | |||
makeUrl(rpcName: string): string { | |||
const urlRpcName = RPC_NAME_REST_MAPPING[rpcName]; | |||
assert(urlRpcName != null, 'Unknown REST mapping for: ' + rpcName); |
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 should be !== 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.
It can be !== undefined
but I tend to not differentiate "undefined" and "null" when I don't need to, so I did the more inclusive != null
check. But I also don't care in the slightest... So switched. :-)
|
||
// RPC Methods have the first character lower-cased | ||
// (e.g. Listen => listen(), BatchGetDocuments => batchGetDocuments()). | ||
const rpcMethod = rpcName.charAt(0).toLowerCase() + rpcName.slice(1); |
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.
Can we use the same manually-provided mapping that we use in webchannel?
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... the mapping is different because the REST endpoints are manually defined (you can see them in firestore.proto if you want). So e.g. the BatchGetDocuments RPC shows up as .batchGetDocuments() in GRPC but as .../batchGet in the REST URL.
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.
Sorry, I wasn't more clear. I meant the mapping technique (a hardcoded map instead of this on-the-fly conversion).
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 the on-the-fly conversion since it means we don't have to hardcode all of the RPCs we use. With WebChannel we can't avoid the hardcoding since the mapping isn't deterministic (whereas I /think/ it is for GRPC).
// trigger the 'finish' event). | ||
remoteEnded = true; | ||
grpcStream.end(); | ||
close(); |
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 now calls the onClose callback, whereas before it didn't. This seems like a requirement to let the Write/Watch stream know that it ended, but I wanted to make sure this is intentional.
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. We used to call grpcStream.end() which would eventually call 'finish' which would call close() [it was convoluted to work around GRPC error handling bugs]. Now we do it directly.
); | ||
}); | ||
|
||
grpcStream.on('status', (status: GrpcStatus) => { | ||
if (!closed) { |
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 obsolete now, isn't 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.
Woops, good catch. Yes. I meant to remove this. (done)
rpcName: string, | ||
request: any, | ||
token: Token | null | ||
): Promise<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.
Can this be of type 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.
Sure can! (done)
@@ -20,6 +20,11 @@ import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; | |||
import * as utilHelpers from '../util/helpers'; | |||
|
|||
describe('WebChannel', () => { |
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.
Can we do something like: (typedef window | describe ? xdescribe)('WebChannel', ...
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.
Apparently (according to https://mochajs.org/), the correct way to skip tests programmatically is:
before(function() {
if (/* check test environment */) {
// setup code
} else {
this.skip();
}
});
But I tried that and it was kinda' crappy (you have to use function() { ... }
instead of () => { ... }
for this
to work, and it only skips the top-level tests, not the nested describe() tests.... So I did it your way. :-)
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.
Thanks! PTAL.
packages/firestore/package.json
Outdated
@@ -18,7 +18,9 @@ | |||
"module": "dist/esm/index.js", | |||
"license": "Apache-2.0", | |||
"dependencies": { | |||
"@firebase/webchannel-wrapper": "^0.2.4" | |||
"@firebase/webchannel-wrapper": "^0.2.4", | |||
"grpc": "^1.6.6", |
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 have now... It turns out grpc now has typings defined so we could remove our hacky ones (I also had to tweak a few things to make our code compatible with the defined typings).
@@ -339,6 +333,8 @@ export class WebChannelConnection implements Connection { | |||
|
|||
// visible for testing | |||
makeUrl(rpcName: string): string { | |||
const urlRpcName = RPC_NAME_REST_MAPPING[rpcName]; | |||
assert(urlRpcName != null, 'Unknown REST mapping for: ' + rpcName); |
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.
It can be !== undefined
but I tend to not differentiate "undefined" and "null" when I don't need to, so I did the more inclusive != null
check. But I also don't care in the slightest... So switched. :-)
|
||
// RPC Methods have the first character lower-cased | ||
// (e.g. Listen => listen(), BatchGetDocuments => batchGetDocuments()). | ||
const rpcMethod = rpcName.charAt(0).toLowerCase() + rpcName.slice(1); |
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... the mapping is different because the REST endpoints are manually defined (you can see them in firestore.proto if you want). So e.g. the BatchGetDocuments RPC shows up as .batchGetDocuments() in GRPC but as .../batchGet in the REST URL.
// trigger the 'finish' event). | ||
remoteEnded = true; | ||
grpcStream.end(); | ||
close(); |
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. We used to call grpcStream.end() which would eventually call 'finish' which would call close() [it was convoluted to work around GRPC error handling bugs]. Now we do it directly.
); | ||
}); | ||
|
||
grpcStream.on('status', (status: GrpcStatus) => { | ||
if (!closed) { |
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.
Woops, good catch. Yes. I meant to remove this. (done)
rpcName: string, | ||
request: any, | ||
token: Token | null | ||
): Promise<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.
Sure can! (done)
@@ -20,6 +20,11 @@ import { DatabaseId, DatabaseInfo } from '../../../src/core/database_info'; | |||
import * as utilHelpers from '../util/helpers'; | |||
|
|||
describe('WebChannel', () => { |
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.
Apparently (according to https://mochajs.org/), the correct way to skip tests programmatically is:
before(function() {
if (/* check test environment */) {
// setup code
} else {
this.skip();
}
});
But I tried that and it was kinda' crappy (you have to use function() { ... }
instead of () => { ... }
for this
to work, and it only skips the top-level tests, not the nested describe() tests.... So I did it your way. :-)
): ProtobufProtoBuilder | undefined { | ||
const builder = protobufjs.newBuilder({ | ||
export function loadProtos(): grpc.GrpcObject { | ||
const options = { | ||
// Beware that converting fields to camel case does not convert the tag | ||
// fields in oneof groups (!!!). |
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.
Does this refer to valueType? ProtobufJS 6.x+ is now doing this.
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.
Interesting. I tried to go down the route of depending directly on protobufjs again and upgrading to 6.x, but it did not seem straightforward (the entire way we're consuming protobufjs has changed an doesn't seem to be documented in the release notes, etc.). So I'm punting on this for now. I did update the comment to mention that upgrading to protobufjs 6.x would resolve this 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.
Please look at the comment in grpc_connection.ts
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.
Thanks!
|
||
// RPC Methods have the first character lower-cased | ||
// (e.g. Listen => listen(), BatchGetDocuments => batchGetDocuments()). | ||
const rpcMethod = rpcName.charAt(0).toLowerCase() + rpcName.slice(1); |
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 the on-the-fly conversion since it means we don't have to hardcode all of the RPCs we use. With WebChannel we can't avoid the hardcoding since the mapping isn't deterministic (whereas I /think/ it is for GRPC).
): ProtobufProtoBuilder | undefined { | ||
const builder = protobufjs.newBuilder({ | ||
export function loadProtos(): grpc.GrpcObject { | ||
const options = { | ||
// Beware that converting fields to camel case does not convert the tag | ||
// fields in oneof groups (!!!). |
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.
Interesting. I tried to go down the route of depending directly on protobufjs again and upgrading to 6.x, but it did not seem straightforward (the entire way we're consuming protobufjs has changed an doesn't seem to be documented in the release notes, etc.). So I'm punting on this for now. I did update the comment to mention that upgrading to protobufjs 6.x would resolve this though.
Hello! When will this be launched in the firebase package on NPM? |
Likely this Thursday. You can keep an eye on https://firebase.google.com/support/release-notes/js to see when it goes out. |
hey thanks for the update! I think there's a problem though. I upgraded to 4.7.0 with npm, and now when I initialize firestore I'm getting the error 'firebase.firestore is not a function' My imports are the same they were before:
And I initialize firestore by calling |
@jmensch1 feel free to open an issue with more context (hopefully a sample repro) of the behavior you are seeing. I was unable to reproduce (just verified across a node build and a browser build). |
necessary after Make event order consistent, and make 'end' and 'error' mutually exclusive grpc/grpc#9101
REST url path (e.g. "batchGet") for WebChannel, and for GRPC just assume
the first letter should be lowercased (e.g. "batchGetDocuments").
the latter accepting a stream of results and aggregating them into an
array (needed to support BatchGetDocuments RPC).
int which GRPC / protobufjs didn't like.
PlatformSupport gets registered.
node.js (before we were integrated with FirebaseAuth).
promise failures.
"--require ts-node/register"