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

Core Data: The "cacheKey" improvements #34771

Closed
Mamaduka opened this issue Sep 13, 2021 · 6 comments
Closed

Core Data: The "cacheKey" improvements #34771

Mamaduka opened this issue Sep 13, 2021 · 6 comments
Labels
[Package] Core data /packages/core-data [Type] Enhancement A suggestion for improvement.

Comments

@Mamaduka
Copy link
Member

Sort _fields and include keys values

Both keys are normalized using the getNormalizedCommaSeparable function, but they aren't sorted. It results in having a different cache key if the order of values is changed, and triggering a new REST API request, even though we're requesting the same data.

Example:

// Do the initial request.
wp.data.select('core').getEntityRecords('taxonomy', 'post_tag', { _fields: 'id,name' });

// This triggers another request, but can use previously cached values.
wp.data.select('core').getEntityRecords('taxonomy', 'post_tag', { _fields: 'name,id' });

Update "hashing" method

Currently, we're using addQueryArgs to generate the query cache key "hash." While this method works just fine, it produces extra characters when encoding commas and ampersands.

I looked at few popular query libraries, and most of them are using JSON.stringify() to create a query cache key. So maybe we should also adopt this method.

/cc @adamziel , @youknowriad, @jsnajdr

@Mamaduka Mamaduka added [Package] Core data /packages/core-data [Type] Enhancement A suggestion for improvement. labels Sep 13, 2021
@jsnajdr
Copy link
Member

jsnajdr commented Sep 13, 2021

JSON.stringify is not going to help with _fields: 'name,id'. The 'name,id' value is a string, and no generic library can understand its internal structure and the fact that the order doesn't matter for _fields. I can imagine a very similar _sort: 'name,id' option where the order matters a lot.

We'll have to treat _fields specially to fix this.

@Mamaduka
Copy link
Member Author

Thanks for the feedback, @jsnajdr.

We're already converting comma-separated values into an array and then back to string for the stableKey. So we can sort them after array conversion.

See:

parts.fields = getNormalizedCommaSeparable( value );
// Make sure to normalize value for `stableKey`
value = parts.fields.join();
}
// Two requests with different include values cannot have same results.
if ( key === 'include' ) {
parts.include = getNormalizedCommaSeparable( value ).map(
Number
);

I only mentioned JSON.stringify as an alternative to the current cache key generator.

// Example cache key using `addQueryArgs`
?include=0%2C1%2C2%2C3%2C4&_fields=id%2Cname

vs

// Same key using `JSON.stringify`
{"include":"0,1,2,3,4","_fields":"id,name"}

@adamziel
Copy link
Contributor

adamziel commented Mar 14, 2022

There are two levels to consider here

Level 1 is @wordpress/data resolution cache. It is entity-agnostic and uses the serialize resolver arguments as keys:

export function selectorArgsToStateKey( args: unknown[] | null | undefined ) {
if ( args === undefined || args === null ) {
return [];
}
const len = args.length;
let idx = len;
while ( idx > 0 && args[ idx - 1 ] === undefined ) {
idx--;
}
return idx === len ? args : args.slice( 0, idx );
}

I don't think it should know about the core-data-specific selectors, so we're left with layer 2: The getEntityRecord resolver in @wordpress/core-data:

export const getEntityRecords = ( kind, name, query = {} ) => async ( {
dispatch,
} ) => {

It would have to compile the full query in

const path = addQueryArgs( entityConfig.baseURL, {
...entityConfig.baseURLParams,
...query,
} );
and e.g. always sort the _fields query arg so that this fragment returns before the HTTP request:

query = { ...query, include: [ key ] };
// The resolution cache won't consider query as reusable based on the
// fields, so it's tested here, prior to initiating the REST request,
// and without causing `getEntityRecords` resolution to occur.
const hasRecords = select.hasEntityRecords( kind, name, query );
if ( hasRecords ) {
return;
}

The same would be needed in getEntityRecords, only it is more complex there because there is no code path to short-circuit if there's a cache hit:

const path = addQueryArgs( entityConfig.baseURL, {
...entityConfig.baseURLParams,
...query,
} );
let records = Object.values( await apiFetch( { path } ) );

@Mamaduka
Copy link
Member Author

Thanks for the feedback, @adamziel.

I agree that "Level 1" doesn't need to know about the core-data specifics, but I would also expect any two selectors with the same arguments to return the same results.

@jsnajdr did some work to improve this (#38914, #38945).

@adamziel
Copy link
Contributor

adamziel commented Mar 16, 2022

I would also expect any two selectors with the same arguments to return the same results.

I assume you meant "two selector calls", not "two selectors"? I guess it's a matter of how we define the same arguments. It would surely be nice if a selector could say "this is a string" and "this is an http query". Perhaps if it made its own resolution cache key? I'm thinking of something like getEntityRecords.resolutionCacheKey = function(kind, name, query) { }

@Mamaduka
Copy link
Member Author

Closing this issue as it doesn't seem to be a priority at the moment.

@Mamaduka Mamaduka closed this as not planned Won't fix, can't repro, duplicate, stale May 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Core data /packages/core-data [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

No branches or pull requests

3 participants