Skip to content

Stop using WebChannelConnection in Lite SDK #3482

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

Merged
merged 11 commits into from
Aug 4, 2020
Merged

Conversation

schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Jul 24, 2020

This PR changes the Lite SDK to use the native networking stack in favor of WebChannel. It uses fetch on Browsers, and node-fetch on Node (which is API compatible with fetch).

Most of the code in this PR is to plumb new platform settings through (which I am not happy about, but I wasn't able to come up with another approach). The main problem is that the Node Lite SDK needs to use Proto3-Json if we switch away from WebChannel, which means that we need to change all callsites for newSerializer. To make this even worse, I added a test-only environment variable to allow me to dynamically import the correct platform. Suggestions welcome.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Jul 25, 2020

Binary Size Report

Affected SDKs

  • @firebase/firestore

    Type Base (2fa0353) Head (4ca5ca1) Diff
    browser 247 kB 247 kB +307 B (+0.1%)
    esm2017 193 kB 193 kB +202 B (+0.1%)
    main 472 kB 472 kB +262 B (+0.1%)
    module 244 kB 245 kB +283 B (+0.1%)
    react-native 193 kB 194 kB +190 B (+0.1%)
  • @firebase/firestore/exp

    Type Base (2fa0353) Head (4ca5ca1) Diff
    browser 187 kB 187 kB +202 B (+0.1%)
    main 464 kB 465 kB +262 B (+0.1%)
    module 187 kB 187 kB +202 B (+0.1%)
    react-native 187 kB 187 kB +190 B (+0.1%)
  • @firebase/firestore/lite

    Type Base (2fa0353) Head (4ca5ca1) Diff
    browser 67.9 kB 63.3 kB -4.58 kB (-6.8%)
    main 144 kB 141 kB -3.08 kB (-2.1%)
    module 67.9 kB 63.3 kB -4.58 kB (-6.8%)
    react-native 67.9 kB 63.4 kB -4.54 kB (-6.7%)
  • @firebase/firestore/memory

    Type Base (2fa0353) Head (4ca5ca1) Diff
    browser 185 kB 186 kB +289 B (+0.2%)
    esm2017 145 kB 145 kB +206 B (+0.1%)
    main 347 kB 347 kB +262 B (+0.1%)
    module 183 kB 184 kB +291 B (+0.2%)
    react-native 145 kB 145 kB +192 B (+0.1%)
  • firebase

    Type Base (2fa0353) Head (4ca5ca1) Diff
    firebase-firestore.js 286 kB 286 kB +254 B (+0.1%)
    firebase-firestore.memory.js 226 kB 226 kB +254 B (+0.1%)
    firebase.js 819 kB 819 kB +255 B (+0.0%)

Test Logs

@schmidt-sebastian
Copy link
Contributor Author

Looks like I need to plumb the JSON serialization settings through to all places where we do serialization. Back to drawing board.

@changeset-bot
Copy link

changeset-bot bot commented Jul 25, 2020

💥 No Changeset

Latest commit: c38f0fa

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

If these changes should be published to npm, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt/rest branch 4 times, most recently from 8e04faa to 46c46df Compare July 31, 2020 03:05
* limitations under the License.
*/

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
Copy link
Member

Choose a reason for hiding this comment

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

How about using a fetch polyfill e.g. isomorphic-fetch, so browser and node can share implementation? So this file can be removed and node_lite/connection.ts becomes:

import `isomorphic-fetch`;

export * from '../browser_lite/connection';

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the idea. I ended up using node-fetch, because:

  • isomorphic-fetch is not maintained anymore
  • node-fetch is not a Polyfill. I think it is better if we don't inject our own code into the global namspace.

return node.newConnection(databaseInfo);
return isLite()
? nodeLite.newConnection(databaseInfo)
: node.newConnection(databaseInfo);
} else if (isReactNative()) {
return rn.newConnection(databaseInfo);
Copy link
Member

Choose a reason for hiding this comment

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

Can we add the isLite() check for RN for completeness? I know we won't hit this code path currently. and similarly in platform/serializer.ts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually reworked this code. We would have to change every callsite and it gets quite ugly with all those branches. Instead, I am now using an environment variable to set the platform for ts-node. It feels a little less ugly to me, but opinions can differ here...

);
}

return JSON.parse(await response.text());
Copy link
Member

Choose a reason for hiding this comment

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

Is it different than just await response.json()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. Shortened.

@@ -16,7 +16,7 @@
*/

import * as firestore from '@firebase/firestore-types';
import { Deferred } from '@firebase/util';
import { Deferred } from '../../util/promise';
Copy link
Member

Choose a reason for hiding this comment

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

Can we just use Deferred from @firebase/util?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only usage of the Deferred promise in Firestore. Since I realized this is test-only, it probably doesn't matter much. I can revert this change with my next update to this PR (I just got the CI to green after one week...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed this change.

Comment on lines 132 to 137
try {
await connection.invokeRPC('RunQuery', {}, null);
fail();
} catch (e) {
expect(e.code).to.equal(Code.UNKNOWN);
}
Copy link
Member

Choose a reason for hiding this comment

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

Does this work?

expect(connection.invokeRPC('RunQuery', {}, null)).to.be.eventually.rejectedWith(Error).and.have.property('code', Code.UNKNOWN);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated (with a simplification)

@@ -24,6 +24,8 @@ import '../../index';
* https://github.com/webpack-contrib/karma-webpack#alternative-usage
*/

process.env.TEST_PLATFORM = 'browser';
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need to set it if if it is the browser test? TEST_PLATFORM is only used for node test, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes... I had to change the webpack bundler to allow me to inject browser-lite. With that change, we are now using the new networking stack for the Lite test. The change in this file was meant to do this, but was 100% wrong (but with the best of intentions!).

urlRpcName !== undefined,
'Unknown REST mapping for: ' + rpcName
);
const path = ((req as unknown) as Indexable).parent || this.databaseRoot;
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like it could be more self-contained if the caller passed the path they wanted based on the RPC they were invoking. As it stands this is kind of magical because Indexable doesn't say anything about how the type should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed the callsites to pass in the path and also removed the manual deletion of parent and database.

@@ -243,9 +248,9 @@ export function mapCodeFromHttpStatus(status: number): Code {
return Code.OK;

case 400: // Bad Request
return Code.INVALID_ARGUMENT;
return Code.FAILED_PRECONDITION;
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem right. A 400 status most typically indicates that the client has sent a bad request, not that the request is valid but can't be performed now. INVALID_ARGUMENT is the primary status in https://cloud.google.com/apis/design/errors#handling_errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So we currently send this request to write a non-existing document in a transaction:

  writes: [
    {
      update: {
        name: 'projects/test-emulator/databases/(default)/documents/test-collection/noHB7gQj6Tp98raxkpxU',
        fields: { counter: { integerValue: '1' } }
      },
      currentDocument: { updateTime: '1970-01-01T00:00:00.000000000Z' }
    }
  ]

This updateTime: epoch trick/hack seems to do the same thing as { currentDocument: { exists: false }} but it returns a 400 (against Prod and the emulator). If we return INVALID_ARGUMENT here, the transaction is not retried.

One thing to note is that mapCodeFromHttpStatus was actually never used before, so at least this change should not affect any existing callsites.

* Base class for all Rest-based connections to the backend (WebChannel and
* HTTP).
*/
export abstract class RestConnection implements Connection {
Copy link
Contributor

Choose a reason for hiding this comment

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

Within any given build isn't the case that there will ever be one connection type? It seems like the bundler could pick which "instance" you get and we could make these all free functions. Or is that coming in a separate PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can do this either way, but I thought it would be cleaner to use inheritance here. There will only ever be one implementation for this class in any given bundle.

Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

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

LGTM

@wilhuff wilhuff removed their assignment Aug 4, 2020
@schmidt-sebastian schmidt-sebastian merged commit 7f9b3d9 into master Aug 4, 2020
@schmidt-sebastian schmidt-sebastian deleted the mrschmidt/rest branch August 4, 2020 21:21
@firebase firebase locked and limited conversation to collaborators Sep 4, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants