Skip to content
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

Changed typings of updateQuery's previousQueryResult to be potentially undefined #12276

Merged
merged 39 commits into from
Feb 6, 2025

Conversation

Cellule
Copy link

@Cellule Cellule commented Jan 15, 2025

Related to #12228

There's an outstanding issue where previousQueryResult sometimes, flakily, ends up undefined
This change aims to have the typings reflect the actual runtime behavior allowing devs to write safe code that can handle the missing previousQueryResult
An alternative could be to simply not call mapFn if previousQueryResult is missing. While neither is ideal nor fixes the underlying issue of "why" previousQueryResult is undefined. At least it would avoid bad crashes.

I've been rolling with this patch for years now and I haven't seen any bad side-effects so far. As far as I can tell, the case where previousQueryResult can be safely ignored and is likely going to be called again with actual data when it matters

Typings Update for updateQuery:

  • Changed typings of updateQuery's previousQueryResult to be potentially undefined in @apollo/client.
  • Updated updateQuery method in src/core/ObservableQuery.ts to reflect the new typings.
  • Modified UpdateQueryFn type in src/core/watchQueryOptions.ts to allow previousQueryResult to be undefined.
  • Updated ObservableQueryFields interface in src/react/types/types.ts to reflect the new typings.

VSCode Configuration:

  • Added editor.codeActionsOnSave setting to disable organizing imports on save in .vscode/settings.json.

Copy link

netlify bot commented Jan 15, 2025

👷 Deploy request for apollo-client-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit b817562

@svc-apollo-docs
Copy link

svc-apollo-docs commented Jan 15, 2025

⚠️ Docs preview not attached to branch

The preview was not built because the PR's base branch release-3.13 is not in the list of sources.

An Apollo team member can comment one of the following commands to dictate which branch to attach the preview to:

  • !docs set-base-branch version-2.6
  • !docs set-base-branch main

Build ID: f38bc6ce53417f54a5403a5f

Copy link

changeset-bot bot commented Jan 15, 2025

🦋 Changeset detected

Latest commit: b817562

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@apollo/client Minor

Not sure what this means? Click here to learn what changesets are.

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

@Cellule Cellule force-pushed the update-query-typings branch from 3a4c44f to d9cfe5d Compare January 15, 2025 17:28
Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

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

I'm generally ok with this change, but would you be willing to add a test that demonstrates the case where previousData is undefined? I'd like to make sure its "documented" in some form through runtime behavior, not just the types. That should help us prevent regressions in the future. That would also help determine whether this is an actual bug, or if this behavior was intentional. I'd like to avoid a band-aid to the types just in case previousData was never intended to be undefined.

Thanks for the contribution!

@@ -241,6 +241,9 @@ describe("subscribeToMore", () => {
}
`,
updateQuery: (prev, { subscriptionData }) => {
if (!prev) {
Copy link
Member

@jerelmiller jerelmiller Jan 17, 2025

Choose a reason for hiding this comment

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

I definitely understand why you did this for this test, but unfortunately this makes it more difficult to know if we introduced regressions by accidentally making prev undefined when it should have a value. The assertion below isn't run in that case, so it would appear that the test would still pass (or at the very least, the source of where the test would fail would be further away from the actual problem).

Instead, I'd recommend updating the prev to prev!, that way this test will crash if this ever switches to undefined.

Copy link
Author

Choose a reason for hiding this comment

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

Make sense, I wasn't 100% sure how to handle it, but you're right the expectation is that prev should be provided here

Copy link
Author

@Cellule Cellule left a comment

Choose a reason for hiding this comment

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

would you be willing to add a test that demonstrates the case where previousData is undefined

If you read the issue, the problem is that we can't figure out the repro steps to have undefined at runtime.
It is likely a race condition that might be hard to reproduce in a testing environment.
I admit I haven't tried recently, this is an issue we've faced years ago and kept the patch.
I can try to get a repro, but I can't make any guarantee

@@ -241,6 +241,9 @@ describe("subscribeToMore", () => {
}
`,
updateQuery: (prev, { subscriptionData }) => {
if (!prev) {
Copy link
Author

Choose a reason for hiding this comment

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

Make sense, I wasn't 100% sure how to handle it, but you're right the expectation is that prev should be provided here

@jerelmiller
Copy link
Member

If you read the issue, the problem is that we can't figure out the repro steps to have undefined at runtime.

Ah shoot. I read that and it went right over my head. Let me play around with this a bit more and see if I can think of any way this could happen. I'll push a test if I can, or respond back and let you know that I couldn't come up with something.

@jerelmiller
Copy link
Member

@Cellule the closest I can get to reproducing this is updateQuery passing an empty object {} as previous. Just to double check, the value you're seeing on the crashes is undefined and not access on a nested property when previous is {} correct?

updateQuery calls cache.diff with returnPartialData: true:

const { result } = queryManager.cache.diff<TData>({
query: this.options.query,
variables: this.variables,
returnPartialData: true,
optimistic: false,
});

That returnPartialData: true returns an empty object if there is absolutely no cache data available for the given query. I've never actually seen result return undefined and have been unable to reproduce a situation where it returns undefined instead of {}. I've traced much of the code and am not seeing anything stick out either.

Is there anything else you can tell me about the queries where you see this happen, such as directives used, type policies, etc? Perhaps the combination of one of those things hits this case.

@Cellule
Copy link
Author

Cellule commented Jan 22, 2025

I admit it's been a long time since I've seen the issue because we've mitigated it by always checking if(!prev) return prev!
I am fairly certain prev === undefined
I'll try to get in a bad state on my side

@jerelmiller
Copy link
Member

That would be super helpful if you can. I fear this change is more of a band-aid to the real underlying issue and would love to figure that out if we can. Otherwise this could be a fairly disruptive change to a lot of existing users who could now potentially see a lot of TypeScript errors after upgrading.

@Cellule
Copy link
Author

Cellule commented Jan 22, 2025

Well maybe I was wrong, it seems it might be {} after all (so my patch wouldn't fix anything really)
image

In my case I'm using updateQuery in query.subscribeToMore
In order to repro I'm doing some operations that will cause the subscription to send more data then navigate to another page "quickly-ish"
The other page is still on the same entity, so it will effectively

  • change variables in useQuery
  • Through useEffect it will resubscribe the subscription `
  • updateQuery seems to be called with data from the previous variables for a new query with no data (hence empty object)
{
    "subscriptionData": {
        "data": {
            "messageUpserted": {
                "message": {
                    "id": 9627,
                    "content": {
                        "text": "removed the work order from being on hold.",
                        "tokens": [
                            {
                                "value": "removed the work order from being on hold.",
                                "__typename": "TextToken"
                            }
                        ],
                        "__typename": "TokenizedText"
                    },
                    "author": {
                        "id": 1338,
                        "img": "https://img.maintainx-dev.com/mx-dev-cac1-uploads/static/user_placeholders/RandomPicture4.png",
                        "firstName": "Michael",
                        "lastName": "Ferris",
                        "displayName": "Michael Ferris",
                        "__typename": "User",
                        "alternateImg": null,
                        "availabilityStatus": null
                    },
                    "createdAt": "2025-01-22T19:50:41.676Z",
                    "reactions": [],
                    "__typename": "SystemWorkOrderStatusChangedMessage",
                    "extraData": {
                        "oldStatus": "ON_HOLD",
                        "newStatus": "OPEN",
                        "newStatusVariant": null,
                        "failures": null,
                        "escalatedUsers": [],
                        "escalatedTeams": [],
                        "__typename": "SystemWorkOrderStatusChangedExtraData"
                    }
                },
                "parent": {
                    "id": 876, // Note the id of the parent that the subscription subscribes to
                    "isUnreadForMe": false,
                    "comments": {
                        "totalCount": 0,
                        "unreadCount": 0,
                        "__typename": "MessageThread"
                    },
                    "__typename": "WorkOrder"
                },
                "transcription": null,
                "__typename": "MessageUpsertedSubscription"
            }
        }
    },
    "variables": {
        "noRedirect": false,
        "id": 4, // Note the id in the variables do not match with the data from the subscription.
        "pagination": {
            "cursor": "",
            "limit": 25
        },
        "shouldFetchAutomationInformation": false
    }
}

@jerelmiller
Copy link
Member

jerelmiller commented Jan 22, 2025

it seems it might be {} after all

Ok great! I was hoping that would be the case and this one actually makes a lot more sense to me. I could reproduce this fairly easily in a test by starting a query, subscribing to the subscription, then having the subscription emit an event before the query finished on the network. In this case, no data had been written to the cache from the query, so previous would be that empty object.

I think the right change here might be to wrap TData in DeepPartial since its possible that at any given time, you may only get back partial data. It still may ruffle a few feathers, but it would be a more accurate type. Let me converse with @phryneas to make sure we're in alignment, but I'm fairly confident this is the right TypeScript fix. Assuming so, I think it makes most sense to target this change with 3.13 so we can better call it out in the changelog.

As for the other behavior you're seeing, it looks like updateQuery is called with this.variables on ObservableQuery, which is just a getter for this.options.variables. Perhaps this is a race condition between when the options actually change in ObservableQuery and when you call subscribeToMore again? Perhaps you could log observable.options.variables to see if that value is updated with the new variables.

@Cellule
Copy link
Author

Cellule commented Jan 22, 2025

There's still something that bugs me with the variables
From what I can test, it's possible the variables passed to subscribeToMore.updateQuery do not match at all the variables passed to the subscription.
It seems entirely possible for the ObservableQuery to have receive new variables, but the subscription is still on the old variables

The scenario seems to be as follows

  • Variables are changed (navigation or whatever)
  • useQuery receives new variables and updates the underlying ObservableQuery
  • useEffect(() => query.subscribeToMore(...), [query.variables]) causes the subscription to be slated to be disconnected
  • Data arrives in the subscription, the updateQuery is called for the previous variables, but the cache looks for query data using the new variables (leading to {})
  • useEffect unsubscribes the subscription then resubscribes correctly
    It looks like a race condition, but so far it hasn't been hard to reproduce on my side

Real quick it sounds like it would be "safe" to simply not call mapFn in updateQuery if results === {} || results === undefined.
So far I haven't been able to reproduce the variables mismatch when there's a cache hit, but it sounds possible in theory

Another thing to mention, the SubscribeToMoreOptions.updateQuery implies the variables will have type TSubscriptionVariables but in reality, the variables returned are TVariables (not currently passed in the generics).
This type is simply wrong, while it's unlikely to be used, if the variables of the query and the variable of the subscription differ enough, it can cause bugs. I had to cast it just to confirm the bad state

makeSubscription<
            IWorkOrderDetailsQueryData,
            IWoMessageUpsertedSubscriptionSubscription,
            IWoMessageUpsertedSubscriptionSubscriptionVariables
          >("messageUpsertedSubscription", query.subscribeToMore, {
            document: messageUpsertedSubscription,
            variables: {
              parent: { id: workOrderId, type: IUserMessageParentType.WorkOrder },
            },
            updateQuery(prev, { subscriptionData, variables }) {
              const newMessage = subscriptionData?.data?.messageUpserted?.message;
              const workOrderUpdates = subscriptionData?.data?.messageUpserted?.parent;
              const transcription = subscriptionData.data.messageUpserted?.transcription;
              if (
                (variables as any as IWorkOrderDetailsQueryVariables).id !==
                (workOrderUpdates as any).id
              ) {
                debugger;
                return prev;
              }
              if (!newMessage || workOrderUpdates?.__typename !== "WorkOrder" || !prev?.workOrder) {
                return prev!;
              }

@Cellule
Copy link
Author

Cellule commented Jan 27, 2025

One issue I see with using DeepPartial is that it will then become a typings nightmare to properly return the correct object

image

I wonder if a better alternative is to actually only update the query if it is complete
I had that idea in the beginning because in my opinion, if for whatever reason the previous data is missing or incomplete, I will want to no-op the update.
It does make a difference in the runtime and could potentially affect some legit use case (although I can't think of one) I feel it might be the better/safer approach.
Likely, if you have a good reason to want to write in the cache of an incomplete query, you're more likely to call cache.writeQuery manually than rely on updateQuery

public updateQuery<TVars extends OperationVariables = TVariables>(
    mapFn: (
      previousQueryResult: Unmasked<TData>,
      options: Pick<WatchQueryOptions<TVars, TData>, "variables">
    ) => Unmasked<TData> | undefined
  ): void {
    const { queryManager } = this;
    const { result, complete } = queryManager.cache.diff<Unmasked<TData>>({
      query: this.options.query,
      variables: this.variables,
      returnPartialData: true,
      optimistic: false,
    });

    if (complete && result) {
      const newResult = mapFn(result, {
        variables: (this as any).variables,
      });
      if (newResult) {
        queryManager.cache.writeQuery({
          query: this.options.query,
          data: newResult,
          variables: this.variables,
        });

        queryManager.broadcastQueries();
      }
    }
  }

@jerelmiller
Copy link
Member

I go back and forth on this. This sounds like a reasonable change, but I fear its too much of a breaking change for a patch/minor release. Since we don't provide any additional information in the callback (i.e. whether the previous result is complete), there could be cases where suddenly your callback is no longer called and its difficult to tell why.

That said, the callback does take an options argument as the 2nd argument. Perhaps we can provide the complete flag here and let the user decide whether to use the partial data to return a full result or to ignore the update (i.e. return undefined). At least this gives the best of both worlds and puts it into the user's hands on how to handle it.

Looking at the return type of updateQuery, currently its set to Unmasked<TData>, but it does appear the implementation is ok returning a falsey value and will do nothing if thats the case. We should probably reflect this in the types as well by updating the return type to Unmasked<TData> | undefined so that TypeScript allows this.

The tricky part would be updating the TypeScript type here. It would be ideal if it were something like this:

updateQuery(previous, { complete }) {
  if (complete) {
    previous;
  // ^? TData
  }

  previous;
  // ^? DeepPartial<TData>
}

Thoughts on this?

@Cellule
Copy link
Author

Cellule commented Jan 27, 2025

I go back and forth on this. This sounds like a reasonable change, but I fear its too much of a breaking change for a patch/minor release. Since we don't provide any additional information in the callback (i.e. whether the previous result is complete), there could be cases where suddenly your callback is no longer called and its difficult to tell why.

Yeah this is exactly why I'm on the fence with the change as well

That said, the callback does take an options argument as the 2nd argument. Perhaps we can provide the complete flag here and let the user decide whether to use the partial data to return a full result or to ignore the update (i.e. return undefined). At least this gives the best of both worlds and puts it into the user's hands on how to handle it.

Humm yeah that could work, but indeed the typings would be the tricky part

Looking at the return type of updateQuery, currently its set to Unmasked<TData>, but it does appear the implementation is ok returning a falsey value and will do nothing if thats the case. We should probably reflect this in the types as well by updating the return type to Unmasked<TData> | undefined so that TypeScript allows this.

Right, I did want to update at least the return type which is more accurate and backward compatible.
This is why I have to do this in my code currently updateQuery(prev) { if (!prev) return prev!; ... }

The tricky part would be updating the TypeScript type here. It would be ideal if it were something like this:

updateQuery(previous, { complete }) {
  if (complete) {
    previous;
  // ^? TData
  }

  previous;
  // ^? DeepPartial<TData>
}

Thoughts on this?

Keeping backward compatibility and typescript relation between the 2 variables sounds like a challenge!
I'll try to give it some thoughts, but whatever solution it is likely to cause friction after the update, which I think is unavoidable

@jerelmiller
Copy link
Member

whatever solution it is likely to cause friction after the update, which I think is unavoidable

Definitely unfortunate, but I'd wager its more preferable than a crash in production!

@Cellule Cellule force-pushed the update-query-typings branch from 3188a44 to 179608a Compare January 27, 2025 20:57
@Cellule Cellule changed the base branch from main to release-3.13 January 27, 2025 20:57
Copy link

pkg-pr-new bot commented Jan 27, 2025

npm i https://pkg.pr.new/@apollo/client@12276

commit: b817562

@Cellule
Copy link
Author

Cellule commented Jan 27, 2025

Alright here's my attempt

This shows my reasoning best I think

const updateQuery: Parameters<typeof observable.updateQuery>[0] = jest.fn(
      (previousResult) => {
        expect(previousResult.complete).toBe(true);
        // Type guard
        if (!previousResult.complete) {
          return undefined;
        }
        return { user: { ...previousResult.user, name: "User (updated)" } };
      }
    );

There's no way I can think of to bind the type of a parameter with a type guard on another parameter.
The details can be tweaked, but my idea is to attach a complete property straight on the result. Obviously this doesn't work as-is since the result could have a complete property.
In the callback, if you don't check for prev.complete the type will be Partial<Unmasked<TData>> (I gave up on DeepPartial because it was giving me typescript errors due to type being too complex)

I cleaned up a bunch of types that we're not shared where it should
I fixed the typings for variables in the options to be TVariables and added a new subscriptionVariables: TSubscriptionVariables in the subscribeToMore case

Let me know what you think

@jerelmiller
Copy link
Member

This is turning out to be a much more difficult change than expected 🙃.

Unfortunately adding a complete field to the result itself is a no-go. We want to avoid doing anything with data since it could conflict with the fields returned from the GraphQL result. We do our best to avoid that at all costs.

Unfortunately changing that first argument in any capacity is a breaking change. That said, how urgent is this change for you? We have started working on v4 and we could certainly think about moving this change there. I'd be open to redesigning the updateQuery API to be more type-safe and v4 would give us a chance to make any necessary breaking changes to facilitate this.

Thoughts?

Copy link
Member

@jerelmiller jerelmiller left a comment

Choose a reason for hiding this comment

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

Ok this should be my last round of feedback. This is looking great though otherwise! Really appreciate the back and forth.

Let me know if you'd like me to make the updates and I'd be happy to push the changes so we can get this across the finish line. I'm planning to release a 3.13 beta in the next day or two and would love to have this in there 🙂

* Might have partial or missing data.
*/
complete: false;
previousQueryResult: DeepPartial<Unmasked<TData>> | undefined;
Copy link
Member

Choose a reason for hiding this comment

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

I know the original argument name was previousQueryResult, but can we call this previousData instead? This value isn't really a "query result" (i.e. QueryResult or ApolloQueryResult type) but rather the actual data instead. We use previousData in other places (e.g. useQuery) so it will align nicely there.

Copy link
Author

Choose a reason for hiding this comment

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

Done

Comment on lines 735 to 751
const variables = (this as any).variables;
let updateOptions: UpdateQueryOptions<TData, TVariables>;
if (complete && result) {
updateOptions = {
variables,
complete: true,
previousQueryResult: result,
};
} else {
updateOptions = {
variables,
complete: false,
previousQueryResult: result as DeepPartial<Unmasked<TData>>,
};
}

const newResult = mapFn(result!, updateOptions);
Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing you did this to ensure TypeScript was happy with the values. While that makes sense, I think we're introducing additional bundle size for an internal implementation detail. In this case, I think it is ok to use a type cast so that we can inline these options.

Suggested change
const variables = (this as any).variables;
let updateOptions: UpdateQueryOptions<TData, TVariables>;
if (complete && result) {
updateOptions = {
variables,
complete: true,
previousQueryResult: result,
};
} else {
updateOptions = {
variables,
complete: false,
previousQueryResult: result as DeepPartial<Unmasked<TData>>,
};
}
const newResult = mapFn(result!, updateOptions);
const newResult = mapFn(result!, {
variables: this.variables,
complete: !!complete,
previousData: result
} as UpdateQueryOptions<TData, TVariables>);

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I was really trying to avoid the typecast, but it's true that the runtime is actually the same in both branches

const { queryManager } = this;
const { result } = queryManager.cache.diff<TData>({
const { result, complete } = queryManager.cache.diff<Unmasked<TData>>({
Copy link
Member

Choose a reason for hiding this comment

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

Good find on this type issue! Can we revert this to TData for now? I'd rather fix this at the core rather than this one place since this is an issue for cache.diff in general. This was an oversight on my part when I was updating all the cache types to use Unmask. Let's fix this in a separate PR.

Suggested change
const { result, complete } = queryManager.cache.diff<Unmasked<TData>>({
const { result, complete } = queryManager.cache.diff<TData>({

I think the type cast suggestion in my other comment will likely "fix" the TypeScript error you see when reverting this.

Copy link
Author

Choose a reason for hiding this comment

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

This type change also affects the first argument of the mapFn which was being cast to Unmasked<TData>
In essence I just moved the cast from 1 place to another
If I change that back to TData then I'll have to put back result! as Unmasked<TData> below

Copy link
Member

Choose a reason for hiding this comment

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

Let's add a TODO comment then so I don't forget to remove this when I get around to fixing that type if thats ok with you.

Copy link
Author

Choose a reason for hiding this comment

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

Ah ok I understand better your point, you mean cache.diff<TData> should likely return type Unmasked<TData> for result. Ok fair I'll revert

>();
}

return undefined;
Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, I'd like to avoid the need to return undefined explicitly in the case that you want to skip the update as this is just noise for no reason. Trying this out locally, looks like if we switch the return type to return | void instead of | undefined, this will work as expected. Can we do that instead so that users can just return early without the undefined value or omit return altogether?

Copy link
Author

Choose a reason for hiding this comment

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

Fair, I considered it but wasn't sure if we preferred it

src/core/watchQueryOptions.ts Outdated Show resolved Hide resolved
@@ -164,31 +164,98 @@ export interface FetchMoreQueryOptions<TVariables, TData = any> {
context?: DefaultContext;
}

export type UpdateQueryFn<
export interface UpdateQueryFn<
Copy link
Member

Choose a reason for hiding this comment

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

Since this type is publicly exported, this change is a breaking change to this type so I think we'll unfortunately need to leave this alone. If we need to migrate away from it in favor of a new name, let's deprecate it instead and direct users to use the new type (I think SubscribeToMoreUpdateQueryFn replaces this if I'm reading it right). That or I'd revert this change completely and we can change the name of this in v4.

Copy link
Author

Choose a reason for hiding this comment

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

I'm confused by my changes at this point, will push the other fixes and review this

Copy link
Author

Choose a reason for hiding this comment

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

I checked again and I'm not 100% sure anymore. Public types are types exported from src/core/index.ts right? If so then the type we need to be backward compatible is UpdateQueryOptions not UpdateQueryFn
I essentially added complete and previousData to UpdateQueryOptions which I think is backward compatible right?
What are your rules when it comes to exporting types publicly?
I think I added a few, maybe too many?

src/react/hooks/__tests__/useBackgroundQuery.test.tsx Outdated Show resolved Hide resolved
return internalQueryRef.observable.subscribeToMore(options);
return internalQueryRef.observable.subscribeToMore(
// TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here
options as any as SubscribeToMoreOptions<TData, OperationVariables>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
options as any as SubscribeToMoreOptions<TData, OperationVariables>
options as SubscribeToMoreOptions<TData, OperationVariables>

Looks like this works without needing to cast to as first 🙂

Copy link
Author

Choose a reason for hiding this comment

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

I think you are right about the other call sites, but here there seems to be an incompatibility due to TSubscriptionData vs TData

Conversion of type 'SubscribeToMoreOptions<TData, TSubscriptionVariables, TSubscriptionData, TVariables>' to type 'SubscribeToMoreOptions<TData, OperationVariables, TData, OperationVariables>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Type 'TSubscriptionData' is not comparable to type 'TData'.
    'TData' could be instantiated with an arbitrary type which could be unrelated to 'TSubscriptionData'.ts(2352)

Copy link
Member

Choose a reason for hiding this comment

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

Ah shoot. I tried this locally in a few areas and they all worked but I must not have tried this one 😆

src/react/hooks/useBackgroundQuery.ts Outdated Show resolved Hide resolved
subscribeToMore: internalQueryRef.observable.subscribeToMore,
// TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here
subscribeToMore: internalQueryRef.observable
.subscribeToMore as any as SubscribeToMoreFunction<TData, TVariables>,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
.subscribeToMore as any as SubscribeToMoreFunction<TData, TVariables>,
.subscribeToMore as SubscribeToMoreFunction<TData, TVariables>,

>();
}

return undefined;
Copy link
Author

Choose a reason for hiding this comment

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

Fair, I considered it but wasn't sure if we preferred it

subscriptionData: { data: Unmasked<TSubscriptionData> };
variables?: TSubscriptionVariables;
subscriptionVariables: TSubscriptionVariables | undefined;
Copy link
Author

Choose a reason for hiding this comment

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

Sure I can remove this part

Comment on lines 201 to 203
/**
* @deprecated Use `options.previousQueryResult` instead.
*/
Copy link
Author

Choose a reason for hiding this comment

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

Thanks!

* Might have partial or missing data.
*/
complete: false;
previousQueryResult: DeepPartial<Unmasked<TData>> | undefined;
Copy link
Author

Choose a reason for hiding this comment

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

Done

* @deprecated Use `options.previousQueryResult` instead.
*/
previousQueryResult: Unmasked<TData>,
options: TOptions & UpdateQueryOptions<TData, TVariables>
Copy link
Author

Choose a reason for hiding this comment

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

Done

@@ -538,6 +537,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`,
optimistic: false,
},
(previous) =>
// REVIEW: Code smell here with the `!` and cast, is it possible for previous to be null or have partial data ?
Copy link
Author

Choose a reason for hiding this comment

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

@jerelmiller what do you think about this?
Do you want to keep the comment or remove it and address this separately?

Copy link
Member

Choose a reason for hiding this comment

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

Let's address separately since it was already there. Feel free to remove this for now. cache.updateQuery might be a whole other animal.

const { queryManager } = this;
const { result } = queryManager.cache.diff<TData>({
const { result, complete } = queryManager.cache.diff<Unmasked<TData>>({
Copy link
Author

Choose a reason for hiding this comment

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

This type change also affects the first argument of the mapFn which was being cast to Unmasked<TData>
In essence I just moved the cast from 1 place to another
If I change that back to TData then I'll have to put back result! as Unmasked<TData> below

Comment on lines 735 to 751
const variables = (this as any).variables;
let updateOptions: UpdateQueryOptions<TData, TVariables>;
if (complete && result) {
updateOptions = {
variables,
complete: true,
previousQueryResult: result,
};
} else {
updateOptions = {
variables,
complete: false,
previousQueryResult: result as DeepPartial<Unmasked<TData>>,
};
}

const newResult = mapFn(result!, updateOptions);
Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I was really trying to avoid the typecast, but it's true that the runtime is actually the same in both branches

@@ -164,31 +164,98 @@ export interface FetchMoreQueryOptions<TVariables, TData = any> {
context?: DefaultContext;
}

export type UpdateQueryFn<
export interface UpdateQueryFn<
Copy link
Author

Choose a reason for hiding this comment

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

I'm confused by my changes at this point, will push the other fixes and review this

return internalQueryRef.observable.subscribeToMore(options);
return internalQueryRef.observable.subscribeToMore(
// TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here
options as any as SubscribeToMoreOptions<TData, OperationVariables>
Copy link
Author

Choose a reason for hiding this comment

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

I think you are right about the other call sites, but here there seems to be an incompatibility due to TSubscriptionData vs TData

Conversion of type 'SubscribeToMoreOptions<TData, TSubscriptionVariables, TSubscriptionData, TVariables>' to type 'SubscribeToMoreOptions<TData, OperationVariables, TData, OperationVariables>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
  Type 'TSubscriptionData' is not comparable to type 'TData'.
    'TData' could be instantiated with an arbitrary type which could be unrelated to 'TSubscriptionData'.ts(2352)

@Cellule
Copy link
Author

Cellule commented Feb 5, 2025

Alright I think it mostly looks good.
If you think there are other things that needs to be fixed I won't mind if you decide to take care of it yourself. I will likely not have more time before next week as I'm working on a presentation for Fragment Masking to my company this Friday :)

@jerelmiller
Copy link
Member

Sounds great! Really appreciate the back and forth. I'll make sure this change makes it into the 3.13 beta 🙂.

Hope the presentation goes well! Feel free to ping me on Discord (@jerelmiller) if you've got questions about data masking 🙂. I'd love to answer them!

@jerelmiller jerelmiller merged commit 670f112 into apollographql:release-3.13 Feb 6, 2025
39 of 41 checks passed
@github-actions github-actions bot mentioned this pull request Feb 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants