Skip to content

Remove Cyclic Dependencies #4160

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 51 commits into from
Dec 8, 2020
Merged

Remove Cyclic Dependencies #4160

merged 51 commits into from
Dec 8, 2020

Conversation

schmidt-sebastian
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian commented Dec 3, 2020

This PR should unblock the Console build and should also allow us to revive our old goal to sort our imports.

It also adds a build rule change to block us from introducing circular dependencies in the future (ec5ac56)

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.

Very impressive!

One high level concern I have with the PR as is, is that it's not necessarily obvious where to look for the implementation of a given type. For example, in some cases it's the _impl but in others it's _methods. Also, whether or not this split even exists seems arbitrary.

It also seems that if you squint just right, this is a very similar change to the forward declarations change we made in iOS C++: declare the exported types separately from the actual implementations to drastically reduce the number of files imported by those type declarations. In the iOS implementation, we created a single file that "exported" the types for a package, e.g. model in [model_fwd.h](https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/core/src/model/model_fwd.h). I wonder if a similar approach here could make this feel more regular.

* If foo is not an object, foo is replaced with an object
* containing foo
*/
export class FieldMask {
Copy link
Contributor

Choose a reason for hiding this comment

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

FieldMask is in a separate file on other platforms. In some cases (e.g database.ts) I know circularity is impossible to avoid and so we have to dump them all in a single file, but elsewhere keeping classes separate and matching structure across platforms is helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Works without any adjustments.

* limitations under the License.
*/

export const enum TypeOrder {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that this is in a separate file, maybe give it a comment?

https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/core/src/model/field_value.h#L62 would be suitable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. I actually added a small comment in a later commit, but the one you provided is better.

@@ -196,3 +194,436 @@ export function isDocumentTarget(target: Target): boolean {
target.filters.length === 0
);
}

export abstract class Filter {
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that everything pertaining to queries isn't in a single file, it seems like we could split this further to make it more like the other platforms. While "target" exists as a class there, it doesn't seem like the obvious home for everything else.

Perhaps create:

  • filter.ts for Filter, FieldFilter, the various Filter implementations, and canonify/stringify ops
  • order_by.ts for Direction, OrderBy, and canonify/stringify ops
  • bound.ts for Bound, sortsBeforeDocument, and others

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will address in follow up.

@@ -0,0 +1,61 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

"value serializer" seems like it could include anything we might think to put in the value proto. Would it make sense to move things like Timestamp, bytes, and GeoPoint serialization into here too?

This also matches the trend on the other platforms to split value proto handling into a separate file too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The serialization of these types actually happens in UserDataReader. The Bytes/Timestamp methods in serializer are only used as helpers for the Watch protos (among other things).

I renamed the file to number_serializer for now.

@@ -0,0 +1,40 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

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

PersistenceTransactionMode seems like something you might look for here too.

Edit: it seems like this was addressed in a later commit.

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. Done.

* Sentinel values that can be used when writing document fields with `set()`
* or `update()`.
*/
export abstract class FieldValue {
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't say I really understand the layering model we have here and why this move is desirable.

It seems like conceptually the classic API should only ever point to the lite API and not the other way around. Allowing the other order encourages cycles and the possibility of polluting the lite implementation with classes that can't be tree shaken.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, this is also something that was fixed in a later commit. It is indeed in lite/src/api/field_value

@@ -398,6 +405,148 @@ export function parseSetData(
);
}

export class DeleteFieldValueImpl extends FieldValue {
Copy link
Contributor

Choose a reason for hiding this comment

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

A possible thought to follow up on layering: would it make sense to explicitly break out common code and the classic API as separate paths? That way you could impose layering checks, e.g.

  • lite -> common
  • exp -> (lite, common)
  • classic -> (exp, common)

or similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once all the dust has settled, I would like to move the /exp and /lite files under /src. They only contain API code and the nested imports are not pretty. This is something I will consider then.

// The components module manages the lifetime of dependencies of the Firestore
// client. Dependencies can be lazily constructed and only one exists per
// Firestore instance.

export interface FirestoreService extends _FirebaseService {
Copy link
Contributor

Choose a reason for hiding this comment

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

Comments, please. In particular what's a FirestoreService and how does it differ from a FirebaseFirestore instance?

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 added the following:

/**
 * An interface implemented by FirebaseFirestore that provides
 * compatibility with the usage in this file. 
 * 
 * This interface mainly exists to remove a cyclic dependency.
 */

I will explore doing the same FirebaseFirestore/FirebaseFirestoreImpl pattern in a follow-up PR.

options: SetOptions
): Promise<void>;

export function setDoc<T>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Setting, updating, and adding documents doesn't seem to have anything to do with "queries". Did you mean to move this here given that reference.ts still exists?

Alternatively, if this can't exist in reference.ts to solve circularity issues, maybe create reference_impl.ts to match the structure you've established elsewhere (e.g. with LocalStoreImpl)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, this was also cleaned up in a later commit, which I know is impossible to detect with a PR this size.

I did rename the file to reference_impl.ts as well.

Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

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

Feedback addressed (or punted to follow up work for next week). Sorry about this PR and thanks for the review!

@@ -196,3 +194,436 @@ export function isDocumentTarget(target: Target): boolean {
target.filters.length === 0
);
}

export abstract class Filter {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will address in follow up.

* limitations under the License.
*/

export const enum TypeOrder {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. I actually added a small comment in a later commit, but the one you provided is better.

* If foo is not an object, foo is replaced with an object
* containing foo
*/
export class FieldMask {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Works without any adjustments.

@@ -0,0 +1,61 @@
/**
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The serialization of these types actually happens in UserDataReader. The Bytes/Timestamp methods in serializer are only used as helpers for the Watch protos (among other things).

I renamed the file to number_serializer for now.

@@ -0,0 +1,40 @@
/**
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. Done.

@@ -366,3 +366,57 @@ export class FieldPath extends BasePath<FieldPath> {
return new FieldPath([]);
}
}

export class DocumentKey {
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 swear it did when I looked at it, but it seems to have magically disappeared. Reverted this change.

* Sentinel values that can be used when writing document fields with `set()`
* or `update()`.
*/
export abstract class FieldValue {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, this is also something that was fixed in a later commit. It is indeed in lite/src/api/field_value

@@ -398,6 +405,148 @@ export function parseSetData(
);
}

export class DeleteFieldValueImpl extends FieldValue {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Once all the dust has settled, I would like to move the /exp and /lite files under /src. They only contain API code and the nested imports are not pretty. This is something I will consider then.

// The components module manages the lifetime of dependencies of the Firestore
// client. Dependencies can be lazily constructed and only one exists per
// Firestore instance.

export interface FirestoreService extends _FirebaseService {
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 added the following:

/**
 * An interface implemented by FirebaseFirestore that provides
 * compatibility with the usage in this file. 
 * 
 * This interface mainly exists to remove a cyclic dependency.
 */

I will explore doing the same FirebaseFirestore/FirebaseFirestoreImpl pattern in a follow-up PR.

options: SetOptions
): Promise<void>;

export function setDoc<T>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, this was also cleaned up in a later commit, which I know is impossible to detect with a PR this size.

I did rename the file to reference_impl.ts as well.

@@ -2560,14 +2560,12 @@
);
case 1:
return (
e
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI: This is removed in a commit later.

@@ -68,7 +69,7 @@ export const CACHE_SIZE_UNLIMITED = LRU_COLLECTION_DISABLED;
* Do not call this constructor directly. Instead, use {@link getFirestore}.
*/
export class FirebaseFirestore extends LiteFirestore {
readonly _queue = newAsyncQueue();
readonly _queue: AsyncQueue = newAsyncQueue();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Somehow the AsyncQueue type is required to not break the APIExtractor toolchain.

@schmidt-sebastian schmidt-sebastian changed the title Remove Circular Dependencies Remove Cyclic Dependencies Dec 5, 2020
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

@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2017 Google LLC
* Copyright 2020 Google LLC
Copy link
Contributor

Choose a reason for hiding this comment

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

If you change this back to 2017 and add a newline back after import { debugAssert } this file we be unchanged in this 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.

Done

@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2017 Google LLC
* Copyright 2020 Google LLC
Copy link
Contributor

Choose a reason for hiding this comment

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

If you change this back to 2017 and revert the import reordering, this file could now be unchanged in this 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.

Done

@wilhuff wilhuff assigned schmidt-sebastian and unassigned wilhuff Dec 7, 2020
@schmidt-sebastian schmidt-sebastian merged commit 2541375 into master Dec 8, 2020
@firebase firebase locked and limited conversation to collaborators Jan 8, 2021
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.

3 participants