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

Add DebugEvent system to urql client for devtools and debugging #608

Merged
merged 54 commits into from
Apr 20, 2020

Conversation

andyrichardson
Copy link
Contributor

@andyrichardson andyrichardson commented Mar 12, 2020

About

There are a lot of moving parts going on in Urql exchanges, these include:

  • Operation specific events (i.e. “operation deduped”)
  • Exchange state events (i.e. “cache state has been changed”)

Currently we provide a means for communicating some operation specific events but not all. There are no means to communicate exchange specific events which are not coupled to an operation.

This proposal is the addition of an event target. It's a pub/sub mechanism for exchanges to publish the aforementioned events and for devtools, a user, or any other third party to consume.

Usage

Listening to events in the console:

const client = createClient({ ... });
client.debugTarget.addEventListener(console.log);

Passing the client to a third party library (such as devtools);

myDevTool(client);

Types

Types only apply if a known key is used. This prevents conflicts with core while also allowing for ambiguous types.

If third parties want to add a type, can use declaration merging

import { DebugEventTypes } from 'urql'

declare module "urql" {
  export interface DebugEventTypes {
    newType: { dataProp: string };
  }
}

Todo

  • Disable in prod
  • Add docs on debugging events
  • Add babel plugin to remove debug events in prod bundles (cc @JoviDeCroock)

@changeset-bot
Copy link

changeset-bot bot commented Mar 12, 2020

🦋 Changeset is good to go

Latest commit: dfd7c24

We got this.

This PR includes changesets to release 4 packages
Name Type
@urql/exchange-graphcache Patch
@urql/exchange-multipart-fetch Patch
@urql/exchange-retry Patch
@urql/core 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

Copy link
Contributor

@wgolledge wgolledge left a comment

Choose a reason for hiding this comment

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

This looks awesome!

I'm guessing this could, down the line, remove the need for the devtools exchange?

One more point on the event dispatching - I wonder whether it could make sense to have an optional dispatch system applied to every exchange that could be invoked (if the exchange desires) with a message and optional data (and the type if it can't be inferred)? It could save having to pass the client through exchanges 🤷‍♂

packages/core/src/exchanges/cache.ts Outdated Show resolved Hide resolved
packages/core/src/exchanges/dedup.ts Show resolved Hide resolved
packages/core/src/utils/Target.ts Outdated Show resolved Hide resolved
@andyrichardson
Copy link
Contributor Author

Cheers for the thoughts @wgolledge

I'm guessing this could, down the line, remove the need for the devtools exchange?

Yes totally, instead we could just pass the client to a devtools function.

One more point on the event dispatching - I wonder whether it could make sense to have an optional dispatch system applied to every exchange that could be invoked (if the exchange desires) with a message and optional data (and the type if it can't be inferred)? It could save having to pass the client through exchanges 🤷‍♂

Initially I thought it would be a good idea to only pass the dispatch function but then realised that we already pass the client (which contains the EventTarget) so it felt pretty redundant.

I think most exchanges will end up with a function like this:

const myExchange = ({ client, forward }) => {
  const publishMessage = (m) => client.eventTarget.dispatchEvent({ ...m, source: "myExchange" });
  // ...
}

@wgolledge
Copy link
Contributor

Awesome - yep looks good as far as I can see! Will wait for @kitten and/or @JoviDeCroock to weigh in

@kitten kitten changed the title Add event support to urql client Proposal: Add event support to urql client Mar 12, 2020
packages/core/src/utils/Target.ts Outdated Show resolved Hide resolved
packages/core/src/exchanges/cache.ts Outdated Show resolved Hide resolved
Copy link

@sofiapoh sofiapoh left a comment

Choose a reason for hiding this comment

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

I like this idea, the changes have a pretty minimal impact too.

packages/core/src/exchanges/cache.ts Outdated Show resolved Hide resolved
@andyrichardson andyrichardson force-pushed the add-event-support branch 2 times, most recently from 69c63ec to 3b95f60 Compare March 13, 2020 15:17
@andyrichardson
Copy link
Contributor Author

Update -

A few example of an event we might see:

{
  type: "fetch",
  message: "A network request has been made",
  operation,
}
{
  type: "refetch",
  message: "Attempting to refetch a failed network request",
  operation,
  data: { 
    retries: 1,
  }
}

Required attributes are now:

  • type (UID for event)
  • message (human readable description)
  • operation (associated operation)
  • data (optional additional data)

This also means we need to change our devtools implementation so that we no longer listen via an exchange but instead for events. That way we streamline every message to look similar to the above.

@kitten
Copy link
Member

kitten commented Mar 17, 2020

@andyrichardson this seems like the way to go 👍 in what cases do we need to provide additional data? It'd be great if we kept this more on the untyped side, so that adding new debugging events would be easy.

What I basically mean is, if we make this a log call (naming-wise as well as in how it looks to contributors), then adding new calls could be useful in Jest / development as well as being very flexible when people go and add custom logging/debugging events in new exchanges.

So for instance, if someone goes and makes a retryOrAuth exchange, they may add

log('auth', 'Refreshing the current access token since it's expired);

So for them this would look just like console.log. It's also basically the same as what we currently do, we'd just allow for type to be raw strings as well.

Edit: I just saw that you're already taking string event types into account, but the other tl;dr then: can the call be info or log without being specific to a client? And can we make the inputs be multiple arguments instead of a raw object, so it feels more like console.log, our our invariant / warn helpers?

@andyrichardson
Copy link
Contributor Author

Thanks for the response @kitten!

What I basically mean is, if we make this a log call (naming-wise as well as in how it looks to contributors), then adding new calls could be useful in Jest / development as well as being very flexible when people go and add custom logging/debugging events in new exchanges.

One of the things I want to really make clear with this is that it's an event bus for the purpose of debugging - not for logging events. While making it work like console.log does make things easier to get started, I think it sets a precedent that doesn't really reflect what's going on.

I wouldn't advise people use this for testing as it exposes implementation details. In a browser however, events could very easily be logged to the console.

const client = createClient({ ... });
client.eventTarget.addEventListener(console.log);

Every exchange action is associated with an operation, hiding that information when dispatching the event leaves room for ambiguity (not something you want when debugging).

The implementation of the event target is basically a 1-for-1 copy of EventTarget. If you want to trim things down a little, we could pass dispatchEvent as an argument to exchanges but I don't see too much benefit for that.

It'd be great if we kept this more on the untyped side, so that adding new debugging events would be easy.

Have you tried dispatching a custom event? There's nothing stopping you from doing that - the types are there for type safety and to prevent conflicts between third-party exchanges 👍

@kitten
Copy link
Member

kitten commented Mar 17, 2020

So regardless of this being associated with testing or debugging (those were just other use-cases that came to mind), I believe the client.eventTarget.dispatchEvent naming doesn't immediately indicate that this isn't a core part of the package / client's architecture.

So while the terms "info" or "logging" (the latter indeed being inaccurate) may not be what we want here, I think it may make sense to replace the term "event" here, e.g. client.debugEvents.dispatch or client.info.dispatch, etc.

Overall I believe it should be clear to someone using this or reading our exchanges that this isn't something they need to learn, or something that we have to mention explicitly in the "Concepts" docs to avoid confusion.

To us this is just an addition, but I think using the term "events" in this case without other hints in the code to its purpose could make it be confused with streams, since we use the term "events" frequently to explain what streams/observables are. (ref: https://formidable.com/open-source/urql/docs/concepts/stream-patterns/)

@andyrichardson
Copy link
Contributor Author

andyrichardson commented Mar 17, 2020

Sure thing @kitten I wasn't aware that your issue was with the use of the term eventTarget and I've renamed it to something less generic - debugTarget

To us this is just an addition, but I think using the term "events" in this case without other hints in the code to its purpose could make it be confused with streams, since we use the term "events" frequently to explain what streams/observables are

Events are core to JS (EventTarget, addEventListener, dispatchEvent) and while I agree, some of the naming conflicts are confusing, I think the problem is with our Urql terminology

Copy link
Member

@kitten kitten left a comment

Choose a reason for hiding this comment

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

Ok, I think we're only pending merging #628 into this branch and adding a changeset note for @urql/core.

@kitten kitten changed the title Proposal: Add event support to urql client Add DebugEvent system to urql client for devtools and debugging Mar 17, 2020
@kitten kitten force-pushed the master branch 2 times, most recently from fc1a5ae to 5a34b9e Compare April 9, 2020 23:54
exchanges/graphcache/src/cacheExchange.ts Outdated Show resolved Hide resolved
exchanges/graphcache/src/cacheExchange.ts Show resolved Hide resolved
exchanges/retry/src/retryExchange.ts Outdated Show resolved Hide resolved
packages/core/src/exchanges/fetch.ts Outdated Show resolved Hide resolved
kitten added 8 commits April 17, 2020 17:53
This makes the composition a little nicer, if we also
don't read it in composeExchanges.
The exchange packages depend on @urql/core and will bump their minimum
dependency range to include the new core version anyway.
There's no clear "success" / "error" condition when we get a
result. A result can contain data and errors, hence we should
only log a result as a definitive error if we also don't have
data.
dispatchDebug cannot be undefined anymore (but will be a noop).

We can simply wrap the call expression in a ternary and let Terser/Closure
take care of eliminating the dead variables and assignments. This also
results in a lower chance of having a dead function stick around, which was
defined in the ObjectProperty transform.
@kitten kitten merged commit e6e2fa9 into master Apr 20, 2020
@kitten kitten deleted the add-event-support branch April 20, 2020 12:18
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.

5 participants