From 20126a85d196137ff258e28f65230339f86a0964 Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 9 Apr 2024 13:36:33 +0300 Subject: [PATCH 1/3] getEntityRecords: batch actions --- packages/core-data/src/resolvers.js | 192 +++++++++++++++------------- 1 file changed, 100 insertions(+), 92 deletions(-) diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index bc9a0ce28d9e94..afc7aea807f67b 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -193,107 +193,115 @@ export const getEditedEntityRecord = forwardResolver( 'getEntityRecord' ); */ export const getEntityRecords = ( kind, name, query = {} ) => - async ( { dispatch } ) => { - const configs = await dispatch( getOrLoadEntitiesConfig( kind, name ) ); - const entityConfig = configs.find( - ( config ) => config.name === name && config.kind === kind - ); - if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) { - return; - } - - const lock = await dispatch.__unstableAcquireStoreLock( - STORE_NAME, - [ 'entities', 'records', kind, name ], - { exclusive: false } - ); - - try { - if ( query._fields ) { - // If requesting specific fields, items and query association to said - // records are stored by ID reference. Thus, fields must always include - // the ID. - query = { - ...query, - _fields: [ - ...new Set( [ - ...( getNormalizedCommaSeparable( query._fields ) || - [] ), - entityConfig.key || DEFAULT_ENTITY_KEY, - ] ), - ].join(), - }; + ( { dispatch, registry } ) => { + registry.batch( async () => { + const configs = await dispatch( + getOrLoadEntitiesConfig( kind, name ) + ); + const entityConfig = configs.find( + ( config ) => config.name === name && config.kind === kind + ); + if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) { + return; } - const path = addQueryArgs( entityConfig.baseURL, { - ...entityConfig.baseURLParams, - ...query, - } ); - - let records, meta; - if ( entityConfig.supportsPagination && query.per_page !== -1 ) { - const response = await apiFetch( { path, parse: false } ); - records = Object.values( await response.json() ); - meta = { - totalItems: parseInt( - response.headers.get( 'X-WP-Total' ) - ), - totalPages: parseInt( - response.headers.get( 'X-WP-TotalPages' ) - ), - }; - } else { - records = Object.values( await apiFetch( { path } ) ); - } + const lock = await dispatch.__unstableAcquireStoreLock( + STORE_NAME, + [ 'entities', 'records', kind, name ], + { exclusive: false } + ); - // If we request fields but the result doesn't contain the fields, - // explicitly set these fields as "undefined" - // that way we consider the query "fulfilled". - if ( query._fields ) { - records = records.map( ( record ) => { - query._fields.split( ',' ).forEach( ( field ) => { - if ( ! record.hasOwnProperty( field ) ) { - record[ field ] = undefined; - } - } ); + try { + if ( query._fields ) { + // If requesting specific fields, items and query association to said + // records are stored by ID reference. Thus, fields must always include + // the ID. + query = { + ...query, + _fields: [ + ...new Set( [ + ...( getNormalizedCommaSeparable( + query._fields + ) || [] ), + entityConfig.key || DEFAULT_ENTITY_KEY, + ] ), + ].join(), + }; + } - return record; + const path = addQueryArgs( entityConfig.baseURL, { + ...entityConfig.baseURLParams, + ...query, } ); - } - dispatch.receiveEntityRecords( - kind, - name, - records, - query, - false, - undefined, - meta - ); + let records, meta; + if ( + entityConfig.supportsPagination && + query.per_page !== -1 + ) { + const response = await apiFetch( { path, parse: false } ); + records = Object.values( await response.json() ); + meta = { + totalItems: parseInt( + response.headers.get( 'X-WP-Total' ) + ), + totalPages: parseInt( + response.headers.get( 'X-WP-TotalPages' ) + ), + }; + } else { + records = Object.values( await apiFetch( { path } ) ); + } - // When requesting all fields, the list of results can be used to - // resolve the `getEntityRecord` selector in addition to `getEntityRecords`. - // See https://github.com/WordPress/gutenberg/pull/26575 - if ( ! query?._fields && ! query.context ) { - const key = entityConfig.key || DEFAULT_ENTITY_KEY; - const resolutionsArgs = records - .filter( ( record ) => record[ key ] ) - .map( ( record ) => [ kind, name, record[ key ] ] ); + // If we request fields but the result doesn't contain the fields, + // explicitly set these fields as "undefined" + // that way we consider the query "fulfilled". + if ( query._fields ) { + records = records.map( ( record ) => { + query._fields.split( ',' ).forEach( ( field ) => { + if ( ! record.hasOwnProperty( field ) ) { + record[ field ] = undefined; + } + } ); + + return record; + } ); + } - dispatch( { - type: 'START_RESOLUTIONS', - selectorName: 'getEntityRecord', - args: resolutionsArgs, - } ); - dispatch( { - type: 'FINISH_RESOLUTIONS', - selectorName: 'getEntityRecord', - args: resolutionsArgs, - } ); + dispatch.receiveEntityRecords( + kind, + name, + records, + query, + false, + undefined, + meta + ); + + // When requesting all fields, the list of results can be used to + // resolve the `getEntityRecord` selector in addition to `getEntityRecords`. + // See https://github.com/WordPress/gutenberg/pull/26575 + if ( ! query?._fields && ! query.context ) { + const key = entityConfig.key || DEFAULT_ENTITY_KEY; + const resolutionsArgs = records + .filter( ( record ) => record[ key ] ) + .map( ( record ) => [ kind, name, record[ key ] ] ); + + dispatch( { + type: 'START_RESOLUTIONS', + selectorName: 'getEntityRecord', + args: resolutionsArgs, + } ); + dispatch( { + type: 'FINISH_RESOLUTIONS', + selectorName: 'getEntityRecord', + args: resolutionsArgs, + } ); + } + } finally { + dispatch.__unstableReleaseStoreLock( lock ); } - } finally { - dispatch.__unstableReleaseStoreLock( lock ); - } + } ); }; getEntityRecords.shouldInvalidate = ( action, kind, name ) => { From 5d8bf1bb400635057141ecbdbb2303857d55a07e Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 9 Apr 2024 13:50:48 +0300 Subject: [PATCH 2/3] Redo --- packages/core-data/src/resolvers.js | 138 ++++++++++++++-------------- 1 file changed, 67 insertions(+), 71 deletions(-) diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index afc7aea807f67b..7de2c354ba2bf8 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -193,81 +193,75 @@ export const getEditedEntityRecord = forwardResolver( 'getEntityRecord' ); */ export const getEntityRecords = ( kind, name, query = {} ) => - ( { dispatch, registry } ) => { - registry.batch( async () => { - const configs = await dispatch( - getOrLoadEntitiesConfig( kind, name ) - ); - const entityConfig = configs.find( - ( config ) => config.name === name && config.kind === kind - ); - if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) { - return; - } - - const lock = await dispatch.__unstableAcquireStoreLock( - STORE_NAME, - [ 'entities', 'records', kind, name ], - { exclusive: false } - ); + async ( { dispatch, registry } ) => { + const configs = await dispatch( getOrLoadEntitiesConfig( kind, name ) ); + const entityConfig = configs.find( + ( config ) => config.name === name && config.kind === kind + ); + if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) { + return; + } - try { - if ( query._fields ) { - // If requesting specific fields, items and query association to said - // records are stored by ID reference. Thus, fields must always include - // the ID. - query = { - ...query, - _fields: [ - ...new Set( [ - ...( getNormalizedCommaSeparable( - query._fields - ) || [] ), - entityConfig.key || DEFAULT_ENTITY_KEY, - ] ), - ].join(), - }; - } + const lock = await dispatch.__unstableAcquireStoreLock( + STORE_NAME, + [ 'entities', 'records', kind, name ], + { exclusive: false } + ); - const path = addQueryArgs( entityConfig.baseURL, { - ...entityConfig.baseURLParams, + try { + if ( query._fields ) { + // If requesting specific fields, items and query association to said + // records are stored by ID reference. Thus, fields must always include + // the ID. + query = { ...query, - } ); + _fields: [ + ...new Set( [ + ...( getNormalizedCommaSeparable( query._fields ) || + [] ), + entityConfig.key || DEFAULT_ENTITY_KEY, + ] ), + ].join(), + }; + } - let records, meta; - if ( - entityConfig.supportsPagination && - query.per_page !== -1 - ) { - const response = await apiFetch( { path, parse: false } ); - records = Object.values( await response.json() ); - meta = { - totalItems: parseInt( - response.headers.get( 'X-WP-Total' ) - ), - totalPages: parseInt( - response.headers.get( 'X-WP-TotalPages' ) - ), - }; - } else { - records = Object.values( await apiFetch( { path } ) ); - } + const path = addQueryArgs( entityConfig.baseURL, { + ...entityConfig.baseURLParams, + ...query, + } ); - // If we request fields but the result doesn't contain the fields, - // explicitly set these fields as "undefined" - // that way we consider the query "fulfilled". - if ( query._fields ) { - records = records.map( ( record ) => { - query._fields.split( ',' ).forEach( ( field ) => { - if ( ! record.hasOwnProperty( field ) ) { - record[ field ] = undefined; - } - } ); - - return record; + let records, meta; + if ( entityConfig.supportsPagination && query.per_page !== -1 ) { + const response = await apiFetch( { path, parse: false } ); + records = Object.values( await response.json() ); + meta = { + totalItems: parseInt( + response.headers.get( 'X-WP-Total' ) + ), + totalPages: parseInt( + response.headers.get( 'X-WP-TotalPages' ) + ), + }; + } else { + records = Object.values( await apiFetch( { path } ) ); + } + + // If we request fields but the result doesn't contain the fields, + // explicitly set these fields as "undefined" + // that way we consider the query "fulfilled". + if ( query._fields ) { + records = records.map( ( record ) => { + query._fields.split( ',' ).forEach( ( field ) => { + if ( ! record.hasOwnProperty( field ) ) { + record[ field ] = undefined; + } } ); - } + return record; + } ); + } + + registry.batch( () => { dispatch.receiveEntityRecords( kind, name, @@ -298,10 +292,12 @@ export const getEntityRecords = args: resolutionsArgs, } ); } - } finally { + dispatch.__unstableReleaseStoreLock( lock ); - } - } ); + } ); + } catch ( e ) { + dispatch.__unstableReleaseStoreLock( lock ); + } }; getEntityRecords.shouldInvalidate = ( action, kind, name ) => { From d41dfefbd67d3596c1c1503b7d8afae48f4e76a0 Mon Sep 17 00:00:00 2001 From: Ella Date: Tue, 9 Apr 2024 14:37:42 +0300 Subject: [PATCH 3/3] Fix unit tests --- packages/core-data/src/test/resolvers.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core-data/src/test/resolvers.js b/packages/core-data/src/test/resolvers.js index 544caad0c2dbc4..36265165da96ec 100644 --- a/packages/core-data/src/test/resolvers.js +++ b/packages/core-data/src/test/resolvers.js @@ -143,6 +143,7 @@ describe( 'getEntityRecords', () => { baseURLParams: { context: 'edit' }, }, ]; + const registry = { batch: ( callback ) => callback() }; beforeEach( async () => { triggerFetch.mockReset(); @@ -160,7 +161,7 @@ describe( 'getEntityRecords', () => { // Provide response triggerFetch.mockImplementation( () => POST_TYPES ); - await getEntityRecords( 'root', 'postType' )( { dispatch } ); + await getEntityRecords( 'root', 'postType' )( { dispatch, registry } ); // Fetch request should have been issued. expect( triggerFetch ).toHaveBeenCalledWith( { @@ -191,7 +192,7 @@ describe( 'getEntityRecords', () => { // Provide response triggerFetch.mockImplementation( () => POST_TYPES ); - await getEntityRecords( 'root', 'postType' )( { dispatch } ); + await getEntityRecords( 'root', 'postType' )( { dispatch, registry } ); // Fetch request should have been issued. expect( triggerFetch ).toHaveBeenCalledWith( { @@ -221,7 +222,7 @@ describe( 'getEntityRecords', () => { // Provide response triggerFetch.mockImplementation( () => POST_TYPES ); - await getEntityRecords( 'root', 'postType' )( { dispatch } ); + await getEntityRecords( 'root', 'postType' )( { dispatch, registry } ); // Fetch request should have been issued. expect( triggerFetch ).toHaveBeenCalledWith( {