Skip to content

Be selective about instanceof #4386

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

Closed
wants to merge 1 commit into from
Closed

Conversation

JoviDeCroock
Copy link
Member

This creates a new env export that allows the user to selectively opt-in or opt-out of instanceof checks and any future debug hints we might introduce

@JoviDeCroock JoviDeCroock requested a review from a team as a code owner May 3, 2025 18:24
Copy link

github-actions bot commented May 3, 2025

Hi @JoviDeCroock, I'm @github-actions bot happy to help you with this PR 👋

Supported commands

Please post this commands in separate comments and only one per comment:

  • @github-actions run-benchmark - Run benchmark comparing base and merge commits for this PR
  • @github-actions publish-pr-on-npm - Build package from this PR and publish it on NPM

throw new Error(
`Cannot use ${className} "${stringifiedValue}" from another module or realm.
export function instanceOf(value: unknown, constructor: Constructor): boolean {
if (isProduction()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

With the old version, we only checked isProduction() once, this one does it on every call. Can we prove there is no performance hit via benchmarks?

Copy link
Member Author

@JoviDeCroock JoviDeCroock May 28, 2025

Choose a reason for hiding this comment

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

I mean, I don't think doing a check for boolean === boolean is going to be that much of a performance hit. The code is inline-cacheable etc. It's also a feature as this can now be toggled on selectively for requests. We're never going to have a way without some global build-time variable to pre-compile a function, this is against what we're trying to achieve.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is what i get on my machine:

image

I am not trying to put up any roadblocks, just checking to make sure that we are all aligned on the costs/benefits.

The alternative is something like: #3915 which could be added to v17 and pushed to the latest patch version of v16.

instanceof has been a blocker for a long time, and so I'm not trying to advocate for a particular solution if you feel strongly about the addition of this feature.

@JoviDeCroock
Copy link
Member Author

@github-actions run-benchmark

@yaacovCR
Copy link
Contributor

@github-actions run-benchmark

Looks like GitHub actions have been disabled

@glasser
Copy link
Contributor

glasser commented May 29, 2025

Can I make sure I understand the full story here? Is the following correct?

  • graphql-js relies on instanceof checks for much of its dispatch logic — all the functions like isEnumType use instanceof.
  • This means that if you have multiple copies of graphql installed in your node_modules, behavior will end up confusing as types that subclass types from one copy won't be detected by the other.
  • So graphql-js has an internal instanceOf function to help with this. If this function returns, its return value is always the same as value instanceof constructor, both before and after this PR.
  • However, in the specific case that (a) we are not "in production" and (b) we would have returned false but (c) it looks like the value's constructor has the same name as the provided constructor, we throw an error instead suggesting that you probably have two copies of graphql-js installed.
  • The only change made with this PR is to change what "in production" means.
  • Before this PR, "in production" means "the environment variable NODE_ENV is set to production".
  • After this PR, "in production" means "you did not call setEnv('development') (or if you did call it, you later called setEnv('production').
  • What this means for somebody upgrading is that if you were relying on the detection to tell you that you had multiple copies installed, it actually won't unless you explicitly add a setEnv call. This is arguably a backwards-incompatibility worthy of a major bump.
  • On the other hand, it won't make things that were currently not throwing start to throw.

Is that accurate?

I'm also a bit confused about what problem we're trying to solve — are people reporting that their use of graphql-js throws the error too often? Not enough?

@yaacovCR
Copy link
Contributor

I'm also a bit confused about what problem we're trying to solve — are people reporting that their use of graphql-js throws the error too often? Not enough?

We want to enable ergonomic cross-platform use of this package. To provide this specialized error only while "in development" in a cross-platform way, we need to figure out how to drop the dependency on NODE_ENV to signal "in production."

As long as we depend on NODE_ENV, then for non-Node environments, a build step is necessary to artificially define NODE_ENV, as detailed in: https://www.graphql-js.org/docs/going-to-production/

Other options:

  • dropping this customized message altogether. The argument would be that after all, making sure that multiple copies of graphql-js are not defined is a package manager's job, not the library.
  • decoupling the check for multiple graphql-js packages from the instanceof calls a la suggestion: alternative check for multiple graphql packages #3915.

@glasser
Copy link
Contributor

glasser commented May 29, 2025

So is the goal to completely drop use of NODE_ENV, or just have it not be the only way of setting it? Would it be reasonable to have this PR but using NODE_ENV (if set) for the default value? In that case the change would be backwards-compatible (and we wouldn't even necessarily need to figure it out to unblock v17).

@benjie
Copy link
Member

benjie commented May 30, 2025

One potential consideration of this approach is that it makes it harder (I think?) to perform dead code elimination in a production build; previously if you set the correct envvar, not only would those code paths be skipped but they could be elided from the bundle entirely.

@JoviDeCroock
Copy link
Member Author

JoviDeCroock commented May 30, 2025

DCE is the main benefit yes but... with our changes to support browser where we i.e. check globalThis most default configurations won't remove this code path. We had to add the typeof globalThis.process replace for that to work.

That being said, if we want to explore other avenues like the global symbol check, my main concern is for this to work with

  • older versions (they might not place this sigil)
  • async imports
  • the new tc39 deferred execution proposal

I'll close this as it looks like the consensus is further exploration.

@JoviDeCroock JoviDeCroock deleted the selective-production branch May 30, 2025 05:54
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 4, 2025
Re-implements graphql#4386 with a configurable function variable initialized to a no-op function in production builds. This is apparently optimizable by v8, as it has no performance hit when setEnv('development') is not called.

This should also allow for tree-shaking in production builds, as the development instanceOf function should not be included within the production bundle if setEnv is not called, although this has not been confirmed.

To avoid a performance hit, setEnv('development') calls setInstanceOfCheckForEnv('development') which sets the function variable to developmentInstanceOfCheck. Setting a property on globalThis was tested, but reading the property led to a small performance hit.
yaacovCR pushed a commit to yaacovCR/graphql-js that referenced this pull request Jun 4, 2025
Re-implements graphql#4386 with a configurable function variable initialized to a no-op function in production builds. This is apparently optimizable by v8, as it has no performance hit when setEnv('development') is not called.

This should also allow for tree-shaking in production builds, as the development instanceOf function should not be included within the production bundle if setEnv is not called, although this has not been confirmed.

To avoid a performance hit, setEnv('development') calls setInstanceOfCheckForEnv('development') which sets the function variable to developmentInstanceOfCheck. Setting a property on globalThis was tested, but reading the property led to a small performance hit.
yaacovCR pushed a commit to yaacovCR/graphql-js that referenced this pull request Jun 4, 2025
Re-implements graphql#4386 with a configurable function variable initialized to a no-op function in production builds. This is apparently optimizable by v8, as it has no performance hit when setEnv('development') is not called.

This should also allow for tree-shaking in production builds, as the development instanceOf function should not be included within the production bundle if setEnv is not called, although this has not been confirmed.

To avoid a performance hit, setEnv('development') calls setInstanceOfCheckForEnv('development') which sets the function variable to developmentInstanceOfCheck. Setting a property on globalThis was tested, but reading the property led to a small performance hit.

Co-authored-by: Yaacov Rydzinski <yaacovCR@gmail.com
yaacovCR pushed a commit to yaacovCR/graphql-js that referenced this pull request Jun 4, 2025
Re-implements graphql#4386 with a configurable function variable initialized to a no-op function in production builds. This is apparently optimizable by v8, as it has no performance hit when setEnv('development') is not called.

This should also allow for tree-shaking in production builds, as the development instanceOf function should not be included within the production bundle if setEnv is not called, although this has not been confirmed.

To avoid a performance hit, setEnv('development') calls setInstanceOfCheckForEnv('development') which sets the function variable to developmentInstanceOfCheck. Setting a property on globalThis was tested, but reading the property led to a small performance hit.

Co-Authored-By: Yaacov Rydzinski <yaacovCR@gmail.com
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 4, 2025
Re-implements graphql#4386 with a configurable function variable initialized to a no-op function in production builds. This is apparently optimizable by v8, as it has no performance hit when setEnv('development') is not called.

This should also allow for tree-shaking in production builds, as the development instanceOf function should not be included within the production bundle if setEnv is not called, although this has not been confirmed.

To avoid a performance hit, setEnv('development') calls setInstanceOfCheckForEnv('development') which sets the function variable to developmentInstanceOfCheck. Setting a property on globalThis was tested, but reading the property led to a small performance hit.

Co-Authored-By: Yaacov Rydzinski <yaacovCR@gmail.com>
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 4, 2025
Re-implements graphql#4386 with a configurable function variable initialized to a no-op function in production builds. This is apparently optimizable by v8, as it has no performance hit when setEnv('development') is not called.

This should also allow for tree-shaking in production builds, as the development instanceOf function should not be included within the production bundle if setEnv is not called, although this has not been confirmed.

To avoid a performance hit, setEnv('development') calls setInstanceOfCheckForEnv('development') which sets the function variable to developmentInstanceOfCheck. Setting a property on globalThis was tested, but reading the property led to a small performance hit.

Co-Authored-By: Yaacov Rydzinski <yaacovCR@gmail.com>
yaacovCR added a commit to yaacovCR/graphql-js that referenced this pull request Jun 4, 2025
Re-implements graphql#4386 with a configurable function variable initialized to a no-op function in production builds. This is apparently optimizable by v8, as it has no performance hit when setEnv('development') is not called.

This should also allow for tree-shaking in production builds, as the development instanceOf function should not be included within the production bundle if setEnv is not called, although this has not been confirmed.

To avoid a performance hit, setEnv('development') calls setInstanceOfCheckForEnv('development') which sets the function variable to developmentInstanceOfCheck. Setting a property on globalThis was tested, but reading the property led to a small performance hit.

Co-Authored-By: Yaacov Rydzinski <yaacovCR@gmail.com>
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.

4 participants