diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts index dbe94f739c13..03e70ef346d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.request-manager.ts @@ -7,13 +7,18 @@ import { type DataTypeResponseModel, type UpdateDataTypeRequestModel, } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiDetailDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiDetailDataRequestManager, + UmbManagementApiInflightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiDataTypeDetailDataRequestManager extends UmbManagementApiDetailDataRequestManager< DataTypeResponseModel, UpdateDataTypeRequestModel, CreateDataTypeRequestModel > { + static #inflightRequestCache = new UmbManagementApiInflightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { create: (body: CreateDataTypeRequestModel) => DataTypeService.postDataType({ body }), @@ -21,6 +26,7 @@ export class UmbManagementApiDataTypeDetailDataRequestManager extends UmbManagem update: (id: string, body: UpdateDataTypeRequestModel) => DataTypeService.putDataTypeById({ path: { id }, body }), delete: (id: string) => DataTypeService.deleteDataTypeById({ path: { id } }), dataCache: dataTypeDetailCache, + inflightRequestCache: UmbManagementApiDataTypeDetailDataRequestManager.#inflightRequestCache, }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts index 0d78bce8f36f..ca22ff6cf10f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/server-data-source/document-type-detail.server.request-manager.ts @@ -7,13 +7,18 @@ import { type DocumentTypeResponseModel, type UpdateDocumentTypeRequestModel, } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiDetailDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiDetailDataRequestManager, + UmbManagementApiInflightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiDocumentTypeDetailDataRequestManager extends UmbManagementApiDetailDataRequestManager< DocumentTypeResponseModel, UpdateDocumentTypeRequestModel, CreateDocumentTypeRequestModel > { + static #inflightRequestCache = new UmbManagementApiInflightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { create: (body: CreateDocumentTypeRequestModel) => DocumentTypeService.postDocumentType({ body }), @@ -22,6 +27,7 @@ export class UmbManagementApiDocumentTypeDetailDataRequestManager extends UmbMan DocumentTypeService.putDocumentTypeById({ path: { id }, body }), delete: (id: string) => DocumentTypeService.deleteDocumentTypeById({ path: { id } }), dataCache: documentTypeDetailCache, + inflightRequestCache: UmbManagementApiDocumentTypeDetailDataRequestManager.#inflightRequestCache, }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts index 0ec8095434e8..37d501d9914e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/detail-data.request-manager.ts @@ -1,4 +1,5 @@ import { UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT } from '../server-event/constants.js'; +import type { UmbManagementApiInflightRequestCache } from '../inflight-request/cache.js'; import type { UmbManagementApiDetailDataCache } from './cache.js'; import { tryExecute, @@ -20,6 +21,7 @@ export interface UmbManagementApiDetailDataRequestManagerArgs< update: (id: string, data: UpdateRequestModelType) => Promise>; delete: (id: string) => Promise>; dataCache: UmbManagementApiDetailDataCache; + inflightRequestCache: UmbManagementApiInflightRequestCache; } export class UmbManagementApiDetailDataRequestManager< @@ -28,6 +30,7 @@ export class UmbManagementApiDetailDataRequestManager< UpdateRequestModelType, > extends UmbControllerBase { #dataCache: UmbManagementApiDetailDataCache; + #inflightRequestCache: UmbManagementApiInflightRequestCache; #create; #read; @@ -52,6 +55,7 @@ export class UmbManagementApiDetailDataRequestManager< this.#delete = args.delete; this.#dataCache = args.dataCache; + this.#inflightRequestCache = args.inflightRequestCache; this.consumeContext(UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, (context) => { this.#serverEventContext = context; @@ -73,18 +77,36 @@ export class UmbManagementApiDetailDataRequestManager< let data: DetailResponseModelType | undefined; let error: UmbApiError | UmbCancelError | undefined; + const inflightCacheKey = `read:${id}`; + // Only read from the cache when we are connected to the server events if (this.#isConnectedToServerEvents && this.#dataCache.has(id)) { data = this.#dataCache.get(id); } else { - const { data: serverData, error: serverError } = await tryExecute(this, this.#read(id)); + const hasInflightRequest = this.#inflightRequestCache.has(inflightCacheKey); + + const request = hasInflightRequest + ? this.#inflightRequestCache.get(inflightCacheKey) + : tryExecute(this, this.#read(id)); - if (this.#isConnectedToServerEvents && serverData) { - this.#dataCache.set(id, serverData); + if (!request) { + throw new Error(`Failed to create or retrieve 'read' request for ID: ${id} (cache key: ${inflightCacheKey}). Aborting read.`); } - data = serverData; - error = serverError; + this.#inflightRequestCache.set(inflightCacheKey, request); + + try { + const { data: serverData, error: serverError } = await request; + + if (this.#isConnectedToServerEvents && serverData) { + this.#dataCache.set(id, serverData); + } + + data = serverData; + error = serverError; + } finally { + this.#inflightRequestCache.delete(inflightCacheKey); + } } return { data, error }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts index 69a6dc3af60a..ba1d59f0146c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/detail/index.ts @@ -1,3 +1,3 @@ -export * from './detail-data.request-manager.js'; -export * from './cache.js'; export * from './cache-invalidation.manager.js'; +export * from './cache.js'; +export * from './detail-data.request-manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts index 772b286b1e9d..ae6beba0a6f1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/index.ts @@ -1,4 +1,5 @@ export * from './detail/index.js'; export * from './item/index.js'; export * from './server-event/constants.js'; +export * from './inflight-request/cache.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/inflight-request/cache.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/inflight-request/cache.ts new file mode 100644 index 000000000000..d1ba9a744193 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/inflight-request/cache.ts @@ -0,0 +1,60 @@ +import type { UmbApiResponse } from '@umbraco-cms/backoffice/resources'; + +// Keep internal +type RequestResolvedType = UmbApiResponse<{ data?: ResponseModelType }>; + +/** + * A cache for inflight requests to the Management Api. Use this class to cache requests and avoid duplicate calls. + * @class UmbManagementApiInflightRequestCache + * @template ResponseModelType + */ +export class UmbManagementApiInflightRequestCache { + #entries = new Map>>(); + + /** + * Checks if an entry exists in the cache + * @param {string} key - The ID of the entry to check + * @returns {boolean} - True if the entry exists, false otherwise + * @memberof UmbManagementApiInflightRequestCache + */ + has(key: string): boolean { + return this.#entries.has(key); + } + + /** + * Adds an entry to the cache + * @param {string} key - A unique key representing the promise + * @param {Promise>>} promise - The promise to cache + * @memberof UmbManagementApiInflightRequestCache + */ + set(key: string, promise: Promise>): void { + this.#entries.set(key, promise); + } + + /** + * Retrieves an entry from the cache + * @param {string} key - The ID of the entry to retrieve + * @returns {Promise> | undefined} - The cached promise or undefined if not found + * @memberof UmbManagementApiInflightRequestCache + */ + get(key: string): Promise> | undefined { + return this.#entries.get(key); + } + + /** + * Deletes an entry from the cache + * @param {string} key - The ID of the entry to delete + * @memberof UmbManagementApiInflightRequestCache + */ + delete(key: string): void { + this.#entries.delete(key); + } + + /** + * Clears all entries from the cache + * @memberof UmbManagementApiInflightRequestCache + */ + clear(): void { + this.#entries.clear(); + } +}