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

feat(gatsby-source-wordpress): support multiple instances of plugin #38119

Merged
merged 15 commits into from
Jun 13, 2023

Conversation

ascorbic
Copy link
Contributor

@ascorbic ascorbic commented May 26, 2023

Description

Currently the WordPress plugin can't support multiple instances because it uses a global redux store. This PR instead scopes the store according to the type prefix. It uses AsyncLocalStorage to ensure that each api call uses the correct store. It additionally scopes the cache, by including the prefix in the cache key.

Documentation

Tests

Related Issues

@gatsbot gatsbot bot added the status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer label May 26, 2023
Comment on lines -11 to +12
"@rematch/core": "^1.4.0",
"@rematch/immer": "^1.2.0",
"@rematch/core": "^2.2.0",
"@rematch/immer": "^2.1.3",
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 old version of rematch had completely broken TS types

Copy link
Contributor

@TylerBarnes TylerBarnes May 26, 2023

Choose a reason for hiding this comment

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

nice! it did indeed. I looked at upgrading around a year ago and went through the whole process only to discover rematch was no longer working and I didn't have more time to figure out why 🤦 hopefully you fared better here!

Copy link
Contributor

Choose a reason for hiding this comment

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

good thing is, while they're odd, the tests are fairly comprehensive, so if there's any problem it'll definitely show up in the tests

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 good stuff is in here

)
}

const store = (): Store => asyncLocalStorage.getStore()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We change store from an exported store to a function which returns the correct store for the context

Comment on lines +26 to +28
effects: () => {
return {}
},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's a bug in the Rematch types that mean that if there are no effects, it infers the type of reducers incorrectly, so we need to add all these empty effects

Comment on lines 27 to 44
if (!STORE_MAP.has(typePrefix)) {
STORE_MAP.set(
typePrefix,
init({
models,
plugins: [immerPlugin<IRootModel>()],
})
)
}

const store = STORE_MAP.get(typePrefix)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We get the instance of the store, keyed by the plugin's type prefix

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 great, I love this implementation. async storage is perfect for this.
Looking at this block now I'm wondering if we should throw an error if there are multiple instances that have the same prefix. What do you think? Having 2 instances with the same prefix will break most things in whichever of those instances runs first

Copy link
Contributor

@TylerBarnes TylerBarnes May 26, 2023

Choose a reason for hiding this comment

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

otherwise I think we could possibly get at the plugin instance ID and use that to index each store. In that case multiple instances with the same prefix will work and all their nodes will be mixed together

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good idea. I think using the instance ID as the key means we could also check that there if there's a prefix collision

const store = STORE_MAP.get(typePrefix)

return asyncLocalStorage.run(store, async () =>
hook(helpers, pluginOptions)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Run the hook using AsyncLocalStorage, passing the correct store instance

Comment on lines +112 to +118
const runApiSteps = (steps: Array<Step>, apiName: string): IGatsbyApiHook =>
wrapApiHook(
async (
helpers: GatsbyNodeApiHelpers,
pluginOptions: IPluginOptions
): Promise<void> => runSteps(steps, helpers, pluginOptions, apiName)
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We wrap the function in wrapApiHook, which adds the handling for AsyncLocalStorage

@LekoArts LekoArts added topic: source-wordpress Related to Gatsby's integration with WordPress topic: source-plugins Relates to the Gatsby source plugins (e.g. -filesystem) and removed status: triage needed Issue or pull request that need to be triaged and assigned to a reviewer labels May 30, 2023
import * as steps from "./steps"

const pluginInitApiName = findApiName(`onPluginInit`)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As this only checks for one method name it's a noop, so can be removed

Comment on lines -355 to +357
const previousTypeDefinitions = await cache.get(`previousTypeDefinitions`)
const previousTypeDefsKey = withPluginKey(`previousTypeDefinitions`)

const previousTypeDefinitions = await cache.get(previousTypeDefsKey)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Include the plugin type prefix in the cache key

@ascorbic ascorbic changed the title wip(gatsby-source-wordpress): scope redux store per plugin instance feat(gatsby-source-wordpress): support multiple instances of plugin Jun 7, 2023
@ascorbic ascorbic marked this pull request as ready for review June 7, 2023 10:34
expect(jobsManager.enqueueJob).toMatchSnapshot()
expect(jobsManager.enqueueJob).toHaveBeenCalledWith({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test was failing because it was snapshotting a Promise object, and using AsyncLocalStorage in other tests meant that there were added internal fields on global Promise. The object it's snapshotting is a jest mock, so it makes more sense to use the actual mock assertions instead of snapshotting its internal state

})()
})
beforeAll(
withGlobalStore(done => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the tests and test utils shouldn't need withGlobalStore since they're int tests that are closer to e2e tests. gatsby develop is run in a subprocess and then tests make assertions against queries to the gql server so any store scoping will happen inside that subprocess.
I could be missing something of course, did this fix an issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh is it cause the tests weirdly use fetchGraphQL from within source-wordpress?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, exactly. I've added it in places where we're doing deep imports from the plugin

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe it would be better to refactor out those imports 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

definitely, I did that for content engine in another PR. I can follow up after this PR and bring my changes over from there

Comment on lines 21 to 32
field.type.name = `JSON`
return {
...field,
type: new GraphQLScalarType({
name: `JSON`,
}),
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

field.type, type.name etc are now immutable in graphql.js, so these were throwing errors in tests.

Comment on lines +1 to +11
/**
*
* @param {Object} options
* @param {string} options.url
* @param {string} options.query
* @param {Object} options.variables
* @param {Object} options.headers
*
* @returns {Promise<Object>}
*/
exports.fetchGraphql = async ({ url, query, variables = {}, headers = {} }) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same signature as the one we were previously importing from the plugin

Comment on lines +55 to +62
export const buildTypeName = (name, prefix) => {
if (!name || typeof name !== `string`) {
return null
}

const {
schema: { typePrefix: prefix },
} = getPluginOptions()
if (!prefix) {
prefix = getPluginOptions().schema.typePrefix
}
Copy link
Contributor Author

@ascorbic ascorbic Jun 8, 2023

Choose a reason for hiding this comment

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

Allowing the prefix to be passed in rather than needing to call getPluginOptions means we can remove the need to use the store at query time.

Comment on lines 77 to 79
typeDef.resolveType = node =>
node?.__typename ? buildTypeName(node.__typename) : null
node?.__typename ? buildTypeName(node.__typename, prefix) : null
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

During develop, this resolver is called outside of the async scope of the createSchemaCustomization api, so we'll instead pass the prefix in from this scope

@ascorbic
Copy link
Contributor Author

ascorbic commented Jun 8, 2023

@TylerBarnes do you know what would cause this failure?

https://app.circleci.com/pipelines/github/gatsbyjs/gatsby/90667/workflows/6d61235c-2dfb-48de-9f2c-134c74b52ccd/jobs/1095663?invite=true#step-109-5999

@TylerBarnes
Copy link
Contributor

TylerBarnes commented Jun 8, 2023

@ascorbic for the yoast test serializes to the same string makes me think somehow the Gatsby resolvers are returning a different order than WPGraphQL for seo.breadcrumbs. Maybe we should switch it for .toMatchObject or something?

Though it also looks like a bunch of the Gatsby schema differences in the second failed test are for Yoast types 🤔 so maybe there's a change in here somewhere that's unexpectedly changing how the schema is generated?

@ascorbic
Copy link
Contributor Author

ascorbic commented Jun 9, 2023

@TylerBarnes Thanks. The Yoast one was fixed by 58ba99e but I've had less luck with the other test. I'm not sure that one is Yoast-related, because some of the types are unrelated. Can you explain a bit more about the test? What would have changed the schema?

@TylerBarnes
Copy link
Contributor

@ascorbic unfortunately as I'm sure you've experienced the codebase for this plugin is complicated to say the least.
It's hard to say what specifically would cause this but I know small changes to how it runs can end up changing the schema drastically.
I ran the tests locally on main and they passed, then I switched to this branch and re-ran and they're failing so it must be some change in here.
It's probably easiest to make a debug branch and then bring over pieces of this PR one commit at a time to figure out what's causing the tests to fail

@ascorbic
Copy link
Contributor Author

ascorbic commented Jun 12, 2023

@TylerBarnes I think I've solved it! 29dd03e

Copy link
Contributor

@TylerBarnes TylerBarnes left a comment

Choose a reason for hiding this comment

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

oh heck yes! 🕺 nice work 🙌

@LekoArts LekoArts merged commit 77e4e19 into gatsbyjs:master Jun 13, 2023
@ascorbic ascorbic deleted the wp-store-context branch June 13, 2023 07:12
@ascorbic
Copy link
Contributor Author

Oh, and thanks for the comprehensive tests that caught this, @TylerBarnes !

@ThyNameIsMud
Copy link
Contributor

@ascorbic @TylerBarnes I'm having some async issues after the upgrade to 7.11.0 of gatsby-source-wordpress. I resolved this after downgrading to 7.10.1, and based on the error, this update is the cause.

Error: Store not found

  • store.ts:56 getStore
    [cree-component-library]/[gatsby-source-wordpress]/src/store.ts:56:11

  • helpers.js:31 getQueryInfoByTypeName
    [cree-component-library]/[gatsby-source-wordpress]/src/steps/source-nodes/helpers.js:31:35

  • transform-object.js:70 resolver
    [cree-component-library]/[gatsby-source-wordpress]/src/steps/create-schema-customization/transform-fields/transform-object.js:70:45

  • resolvers.ts:751 wrappedTracingResolver
    [cree-component-library]/[gatsby]/src/schema/resolvers.ts:751:20

  • async Promise.all

  • graphql-runner.ts:255 GraphQLRunner.query
    [cree-component-library]/[gatsby]/src/query/graphql-runner.ts:255:14

  • query-runner.ts:140 queryRunner
    [cree-component-library]/[gatsby]/src/query/query-runner.ts:140:14

This causes the worker to exit without finishing. Would you happen to have any insights on what's going on here, purhapes a way to debug to understand why it's losing the context of the store?

@cavemon
Copy link

cavemon commented Jan 20, 2024

@ascorbic @TylerBarnes I'm having some async issues after the upgrade to 7.11.0 of gatsby-source-wordpress. I resolved this after downgrading to 7.10.1, and based on the error, this update is the cause.

Error: Store not found

  • store.ts:56 getStore
    [cree-component-library]/[gatsby-source-wordpress]/src/store.ts:56:11
  • helpers.js:31 getQueryInfoByTypeName
    [cree-component-library]/[gatsby-source-wordpress]/src/steps/source-nodes/helpers.js:31:35
  • transform-object.js:70 resolver
    [cree-component-library]/[gatsby-source-wordpress]/src/steps/create-schema-customization/transform-fields/transform-object.js:70:45
  • resolvers.ts:751 wrappedTracingResolver
    [cree-component-library]/[gatsby]/src/schema/resolvers.ts:751:20
  • async Promise.all
  • graphql-runner.ts:255 GraphQLRunner.query
    [cree-component-library]/[gatsby]/src/query/graphql-runner.ts:255:14
  • query-runner.ts:140 queryRunner
    [cree-component-library]/[gatsby]/src/query/query-runner.ts:140:14

This causes the worker to exit without finishing. Would you happen to have any insights on what's going on here, purhapes a way to debug to understand why it's losing the context of the store?

@ThyNameIsMud Were you able to fix this? I've been running into this issue for months as I'm trying to upgrade from v3 to v5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: source-plugins Relates to the Gatsby source plugins (e.g. -filesystem) topic: source-wordpress Related to Gatsby's integration with WordPress
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants