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

Issue Invalidating Storage #2443

Closed
leggomuhgreggo opened this issue May 13, 2022 · 7 comments · Fixed by #2458
Closed

Issue Invalidating Storage #2443

leggomuhgreggo opened this issue May 13, 2022 · 7 comments · Fixed by #2458
Labels
bug 🐛 Oh no! A bug or unintented behaviour.

Comments

@leggomuhgreggo
Copy link
Contributor

Overview

Trying to validate the Cache Invalidation on Logout functionality, extended with storage.clear() using graphcache + offlineExchange.

Doesn't seem to want to clear as expected.

Repro: codesandbox

[email protected]
@urql/[email protected]

Things I've Tried So Far

I reviewed these related issues [#1684, #297] and looked over the relevant code but nothing jumped out at me.

My IndexedDB chops aren't very sophisticated but I took a swing at manually deleting the db with indexedDB.deleteDatabase(dbName) but no luck (blocked transaction I think, may revisit)

Quite possible I've overlooked something simple.

Additional Context

In case it's useful, my goal is to make sure there's no chance of user PII sticking around in the browser, in case someone is using my app from a public computer, for example.

Thanks!

@leggomuhgreggo leggomuhgreggo added the bug 🐛 Oh no! A bug or unintented behaviour. label May 13, 2022
@nightgrey
Copy link

nightgrey commented May 13, 2022

I have the same issue as you; the DB is just.. not cleared. 👍 Same versions as you.

I use a custom ClientProvider with a resetClient function where I clear the storage - like this:

ClientProvider

/**
 * ClientProvider
 *
 * Provides `urql` client and allows resetting it via `useClient` to clear caches.
 *
 * @see https://github.com/FormidableLabs/urql/issues/297#issuecomment-504782794
 */
export const ClientProvider = ({
  createClient: createClientProp,
  children,
}) => {
  const [client, setClient] = React.useState(createClientProp());

  return (
    <ClientContext.Provider
      value={{
        resetClient: async () => {
          setClient(createClientProp());
          await storage.clear();
        },
        client,
      }}
    >
      <UrqlProvider value={client}>{children}</UrqlProvider>
    </ClientContext.Provider>
  );
};

createClient

const storage = makeDefaultStorage();

export const createClient = (opts) =>
  urqlCreateClient({
    url: `${config.api.urls.gateway.http}/graphql`,
    exchanges: [
      devtoolsExchange,
      dedupExchange,
      offlineExchange({
        schema,
        storage,
        keys: {
          LeadPage: () => null,
          PropertyObjectDetails: () => null,
        },
        resolvers: {
          Queries: {
            lead: (data, args) => ({
              __typename: 'Lead',
              id: args.id,
            }),
          },
        },
      }),
      fetchExchange,
      refocusExchange(),
      requestPolicyExchange(),
    ],
    fetchOptions: () => {
      const accessToken = getAccessToken(computeLocalBundle());

      return {
        headers: { authorization: accessToken ? `Bearer ${accessToken}` : '' },
      };
    },
    ...opts,
  });

On logout and/or before login, I tried to clear the cache, but it just stays the same.

@nightgrey
Copy link

nightgrey commented May 13, 2022

@leggomuhgreggo I had success in clearing IndexedDB via ...

          const transaction = global.indexedDB.deleteDatabase('graphql-cache');
          await new Promise((resolve, reject) => {
            transaction.addEventListener('success', resolve);
            transaction.addEventListener('error', reject);
          });

The database will be deleted. The only issue is that you have to reload the page until the browser does not have access to the data anymore (I guess it is in RAM?). Even though Chrome DevTools doesn't show a DB anymore, the data will still be retrieved from it when urql performs requests.

That is the best workaround I have found yet.

@JoviDeCroock
Copy link
Collaborator

          const transaction = global.indexedDB.deleteDatabase('graphql-cache');
          await new Promise((resolve, reject) => {
            transaction.addEventListener('success', resolve);
            transaction.addEventListener('error', reject);
          });
          setClient(createClientProp());

Are you doing the above? In your code example it looks like you first reconstruct the client which would mean that it still has the oppurtunity to read before it gets cleared/destroyed

@nightgrey
Copy link

nightgrey commented May 13, 2022

@JoviDeCroock

          const transaction = global.indexedDB.deleteDatabase('graphql-cache');
          await new Promise((resolve, reject) => {
            transaction.addEventListener('success', resolve);
            transaction.addEventListener('error', reject);
          });
          setClient(createClientProp());

Are you doing the above? In your code example it looks like you first reconstruct the client which would mean that it still has the oppurtunity to read before it gets cleared/destroyed

Yes, I am - but only because storage.clear() doesn't / didn't work.. but now that you say it, you might be right about the reconstruction...

Here's my whole urql file:

import {
  createClient as urqlCreateClient,
  dedupExchange,
  fetchExchange,
  Provider as UrqlProvider,
} from 'urql';
import { devtoolsExchange } from '@urql/devtools';
import { offlineExchange } from '@urql/exchange-graphcache';
import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage';
import { refocusExchange } from '@urql/exchange-refocus';
import { requestPolicyExchange } from '@urql/exchange-request-policy';
import schema from '../../../schema.json';

// Create a persistent cache storage
const storage = makeDefaultStorage();

/**
 * Create a `urql` client
 *
 * @param {import('urql').ClientOptions} opts
 * @return {Client}
 */
export const createClient = (opts) =>
  urqlCreateClient({
    url: `${config.api.urls.gateway.http}/graphql`,
    exchanges: [
      devtoolsExchange,
      dedupExchange,
      offlineExchange({
        schema,
        storage,
        keys: {
          LeadPage: () => null,
          PropertyObjectDetails: () => null,
        },
        resolvers: {
          Queries: {
            lead: (data, args) => ({
              __typename: 'Lead',
              id: args.id,
            }),
          },
        },
      }),
      fetchExchange,
      refocusExchange(),
      requestPolicyExchange(),
    ],
    fetchOptions: () => {
      const accessToken = getAccessToken(computeLocalBundle());

      return {
        headers: { authorization: accessToken ? `Bearer ${accessToken}` : '' },
      };
    },
    ...opts,
  });

export const ClientContext = React.createContext({
  resetClient: () => {},
  client: null,
});

/**
 * ClientProvider
 *
 * Provides `urql` client and allows resetting it via `useClient` to clear caches.
 *
 * @see https://github.com/FormidableLabs/urql/issues/297#issuecomment-504782794
 */
export const ClientProvider = ({
  createClient: createClientProp,
  children,
}) => {
  const [client, setClient] = React.useState(createClientProp());

  return (
    <ClientContext.Provider
      value={{
        resetClient: async () => {
          setClient(createClientProp());

          await storage.clear();

          // This should not be here in the best case :)
          const transaction = global.indexedDB.deleteDatabase('urql-cache');
          await new Promise((resolve, reject) => {
            transaction.addEventListener('success', resolve);
            transaction.addEventListener('error', reject);
          });
        },
        client,
      }}
    >
      <UrqlProvider value={client}>{children}</UrqlProvider>
    </ClientContext.Provider>
  );
};

ClientProvider.propTypes = {
  createClient: PropTypes.func.isRequired,
  children: PropTypes.element,
};

ClientProvider.defaultProps = {
  children: null,
};

/**
 * UseClient
 *
 * @example
 *   const { resetClient, client } = useClient();
 * @return {{ resetClient, client }}
 */
export const useClient = () => React.useContext(ClientContext);

So, basically what I do is before logging in and when logging out, I call...

import { useClient } from 'app-lib/urql';

// Hook that runs on every login
export const useLoginFlow = () => {
  const { resetClient } = useClient();

  // Other stuff.

  resetClient();
}

// Hook that runs on every logout
export const useLogoutFlow = () => {
  const { resetClient } = useClient();

  // Other stuff.

  resetClient();
}

I'll see if it works without re-creating the client and/or switching the order.

@nightgrey
Copy link

nightgrey commented May 13, 2022

@JoviDeCroock

I reduced resetClient to storage.clear(), but that doesn't clear the storage either. It persists after storage.clear() is executed, which I confirmed by debugging.

image

It is, as far as I can see, executed properly... it just doesn't clear anything.

image

@JoviDeCroock
Copy link
Collaborator

Quickly forked the example to show what I mean, you are dealing with a non-awaited function there because of the iife closure. This means that you will clear when the client is activating or is still active meaning it can still read the store. Here's an example of how this can work sandbox

You will see that initially the data will output an empty object, when we press login it will still log an empty object, after 1 second it will show you the hydrated state, when we press logout you will see an empty object again.

Log output going in the sequence of: logged out (empty object), logged in (empty object), After completed login (hydrated object with pokemon data) and finally logged out (empty object)

@nightgrey
Copy link

nightgrey commented May 16, 2022

Your example does show an empty console.log, but urql does not fetch new data after logging in again. I also implemented this in my project (it works like you show it), but it does not load new data after logging in again.

Check the network tab. It does load the data from cache, and Chrome shows the IndexedDB database also does not get cleared.

This is a video after login:

urql-cache.mp4

... and this is the graphcache after logout:

image

Is this intended behaviour?

What I implemented in my project, for completeness sake

It has the same issue as described above, though.

export const ClientProvider = ({
  createClient: createClientProp,
  children,
}) => {
  const [client, setClient] = React.useState(createClientProp());

  return (
    <ClientContext.Provider
      value={{
        storage,
        createClient: async () => setClient(createClientProp()),
        clearClient: async () => {
          await storage.clear();

          setClient(null);

          const data = await storage.readData();

          console.log(data);
        },
        client,
      }}
    >
      <UrqlProvider value={client}>{children}</UrqlProvider>
    </ClientContext.Provider>
  );
};

In my login and logout flow/functions, I await createClient() and await clearClient() respectively. Anything else did not change. It successfully logs an empty object (and does log a full object when logging after the login, just removed that part again).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Oh no! A bug or unintented behaviour.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants