diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.request-manager.ts index f53db837bddc..3e7f1d59747d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/item/data-type-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { dataTypeItemCache } from './data-type-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { DataTypeService, type DataTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiDataTypeItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => DataTypeService.getItemDataType({ query: { id: ids } }), dataCache: dataTypeItemCache, + inflightRequestCache: UmbManagementApiDataTypeItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.request-manager.ts index a610dac38690..35edc359baf4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/item/dictionary-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { dictionaryItemCache } from './dictionary-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { DictionaryService, type DictionaryItemItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiDictionaryItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => DictionaryService.getItemDictionary({ query: { id: ids } }), dataCache: dictionaryItemCache, + inflightRequestCache: UmbManagementApiDictionaryItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.request-manager.ts index 762f8a9aea1f..d93255d2fa10 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/item/document-blueprint-item.server.request-manager.ts @@ -5,13 +5,19 @@ import { DocumentBlueprintService, type DocumentBlueprintItemResponseModel, } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiDocumentBlueprintItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => DocumentBlueprintService.getItemDocumentBlueprint({ query: { id: ids } }), dataCache: documentBlueprintItemCache, + inflightRequestCache: UmbManagementApiDocumentBlueprintItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.request-manager.ts index 930dbf38ba59..58e668298373 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/item/document-type-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { documentTypeItemCache } from './document-type-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { DocumentTypeService, type DocumentTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiDocumentTypeItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => DocumentTypeService.getItemDocumentType({ query: { id: ids } }), dataCache: documentTypeItemCache, + inflightRequestCache: UmbManagementApiDocumentTypeItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.request-manager.ts index ea31c140a6cc..51ff280eed26 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/repository/document-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { documentItemCache } from './document-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { DocumentService, type DocumentItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiDocumentItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => DocumentService.getItemDocument({ query: { id: ids } }), dataCache: documentItemCache, + inflightRequestCache: UmbManagementApiDocumentItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.request-manager.ts index 744dcb164ee0..bd68bf2e94eb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/repository/item/language-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { languageItemCache } from './language-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { LanguageService, type LanguageItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiLanguageItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (isoCodes: Array) => LanguageService.getItemLanguage({ query: { isoCode: isoCodes } }), dataCache: languageItemCache, + inflightRequestCache: UmbManagementApiLanguageItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.isoCode, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.test.ts new file mode 100644 index 000000000000..4b4b6bf90cb4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.test.ts @@ -0,0 +1,424 @@ +import { UmbManagementApiItemDataRequestManager } from './item-data.request-manager.js'; +import { UmbManagementApiItemDataCache } from './cache.js'; +import { UmbManagementApiInFlightRequestCache } from '../inflight-request/cache.js'; +import { UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT } from '../server-event/constants.js'; +import { expect } from '@open-wc/testing'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; + +interface TestItemModel { + id: string; + name: string; +} + +// Mock server event context +class MockServerEventContext { + #isConnected = new UmbBooleanState(undefined); + isConnected = this.#isConnected.asObservable(); + + setIsConnected(value: boolean | undefined) { + this.#isConnected.setValue(value); + } + + getHostElement() { + return undefined as unknown as Element; + } +} + +@customElement('test-item-request-manager-host') +class UmbTestItemRequestManagerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +describe('UmbManagementApiItemDataRequestManager', () => { + let hostElement: UmbTestItemRequestManagerHostElement; + let manager: UmbManagementApiItemDataRequestManager; + let dataCache: UmbManagementApiItemDataCache; + let inflightRequestCache: UmbManagementApiInFlightRequestCache; + let mockServerEventContext: MockServerEventContext; + + // Mock API function + let mockGetItems: (ids: Array) => Promise<{ data: Array }>; + + beforeEach(async () => { + hostElement = new UmbTestItemRequestManagerHostElement(); + document.body.appendChild(hostElement); + + // Set up mock server event context + mockServerEventContext = new MockServerEventContext(); + new UmbContextProviderController( + hostElement, + UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, + mockServerEventContext as unknown as typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE, + ); + + // Set up caches + dataCache = new UmbManagementApiItemDataCache(); + inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + + // Set up mock API function + mockGetItems = async (ids: Array) => ({ + data: ids.map((id) => ({ id, name: `Item ${id}` })), + }); + + // Create manager + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Allow context consumption to complete + await Promise.resolve(); + }); + + afterEach(() => { + manager.destroy(); + document.body.innerHTML = ''; + }); + + describe('Public API', () => { + it('has a getItems method', () => { + expect(manager).to.have.property('getItems').that.is.a('function'); + }); + }); + + describe('getItems', () => { + it('fetches from the server when not connected to server events', async () => { + let requestedIds: Array = []; + mockGetItems = async (ids: Array) => { + requestedIds = ids; + return { data: ids.map((id) => ({ id, name: `Item ${id}` })) }; + }; + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + const result = await manager.getItems(['item-1', 'item-2']); + + expect(requestedIds).to.deep.equal(['item-1', 'item-2']); + expect(result.data).to.have.lengthOf(2); + }); + + it('returns cached data when connected to server events and items are cached', async () => { + mockServerEventContext.setIsConnected(true); + + let getItemsCalled = false; + mockGetItems = async (ids: Array) => { + getItemsCalled = true; + return { data: ids.map((id) => ({ id, name: `Fresh Item ${id}` })) }; + }; + + // Pre-populate cache + dataCache.set('item-1', { id: 'item-1', name: 'Cached Item 1' }); + dataCache.set('item-2', { id: 'item-2', name: 'Cached Item 2' }); + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Wait for context observation + await new Promise((resolve) => setTimeout(resolve, 10)); + + const result = await manager.getItems(['item-1', 'item-2']); + + expect(getItemsCalled).to.be.false; + expect(result.data).to.have.lengthOf(2); + expect(result.data?.find((i) => i.id === 'item-1')?.name).to.equal('Cached Item 1'); + }); + + it('uses cached items and only fetches non-cached items when connected', async () => { + mockServerEventContext.setIsConnected(true); + + // Pre-populate cache with one item + dataCache.set('item-1', { id: 'item-1', name: 'Cached Item 1' }); + + let requestedIds: Array = []; + mockGetItems = async (ids: Array) => { + requestedIds = ids; + return { data: ids.map((id) => ({ id, name: `Fresh Item ${id}` })) }; + }; + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Wait for context observation + await new Promise((resolve) => setTimeout(resolve, 10)); + + const result = await manager.getItems(['item-1', 'item-2', 'item-3']); + + // Should only request non-cached items + expect(requestedIds).to.deep.equal(['item-2', 'item-3']); + + // Result should contain all items (cached + fetched) + expect(result.data).to.have.lengthOf(3); + + // Verify cached item was returned from cache + const cachedItem = result.data?.find((item) => item.id === 'item-1'); + expect(cachedItem?.name).to.equal('Cached Item 1'); + }); + + it('caches data after fetching when connected to server events', async () => { + mockServerEventContext.setIsConnected(true); + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Wait for context observation + await new Promise((resolve) => setTimeout(resolve, 10)); + + await manager.getItems(['item-1', 'item-2']); + + expect(dataCache.has('item-1')).to.be.true; + expect(dataCache.has('item-2')).to.be.true; + }); + + it('does not cache data after fetching when not connected to server events', async () => { + mockServerEventContext.setIsConnected(false); + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Wait for context observation + await new Promise((resolve) => setTimeout(resolve, 10)); + + await manager.getItems(['item-1', 'item-2']); + + expect(dataCache.has('item-1')).to.be.false; + expect(dataCache.has('item-2')).to.be.false; + }); + + it('deduplicates concurrent getItems calls with overlapping IDs', async () => { + const requestedIdBatches: Array> = []; + mockGetItems = async (ids: Array) => { + requestedIdBatches.push([...ids]); + // Simulate network delay so both calls are concurrent + await new Promise((resolve) => setTimeout(resolve, 50)); + return { data: ids.map((id) => ({ id, name: `Item ${id}` })) }; + }; + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Make concurrent requests with overlapping IDs + const [result1, result2] = await Promise.all([ + manager.getItems(['item-1', 'item-2', 'item-3']), + manager.getItems(['item-2', 'item-3', 'item-4']), + ]); + + // The first call should request all 3 IDs, the second should only request item-4 + // (item-2 and item-3 are already inflight from the first call) + const allRequestedIds = requestedIdBatches.flat(); + expect(allRequestedIds).to.include('item-1'); + expect(allRequestedIds).to.include('item-4'); + // item-2 and item-3 should only appear once across all batches + expect(allRequestedIds.filter((id) => id === 'item-2')).to.have.lengthOf(1); + expect(allRequestedIds.filter((id) => id === 'item-3')).to.have.lengthOf(1); + + // Both results should contain the items they requested + expect(result1.data).to.have.lengthOf(3); + expect(result2.data).to.have.lengthOf(3); + + expect(result1.data?.map((i) => i.id)).to.include.members(['item-1', 'item-2', 'item-3']); + expect(result2.data?.map((i) => i.id)).to.include.members(['item-2', 'item-3', 'item-4']); + }); + + it('makes zero server requests when all IDs are already inflight', async () => { + let getItemsCallCount = 0; + mockGetItems = async (ids: Array) => { + getItemsCallCount++; + await new Promise((resolve) => setTimeout(resolve, 50)); + return { data: ids.map((id) => ({ id, name: `Item ${id}` })) }; + }; + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + const [result1, result2] = await Promise.all([ + manager.getItems(['item-1', 'item-2']), + manager.getItems(['item-1', 'item-2']), + ]); + + // Only one batch API call should have been made + expect(getItemsCallCount).to.equal(1); + + // Both callers should get the same data + expect(result1.data).to.have.lengthOf(2); + expect(result2.data).to.have.lengthOf(2); + expect(result1.data?.map((i) => i.id)).to.include.members(['item-1', 'item-2']); + expect(result2.data?.map((i) => i.id)).to.include.members(['item-1', 'item-2']); + }); + + it('cleans up inflight cache after requests complete', async () => { + mockGetItems = async (ids: Array) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + return { data: ids.map((id) => ({ id, name: `Item ${id}` })) }; + }; + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + await Promise.all([ + manager.getItems(['item-1', 'item-2']), + manager.getItems(['item-2', 'item-3']), + ]); + + // All inflight entries should be cleaned up + expect(inflightRequestCache.has('item:item-1')).to.be.false; + expect(inflightRequestCache.has('item:item-2')).to.be.false; + expect(inflightRequestCache.has('item:item-3')).to.be.false; + }); + + it('combines data cache, inflight, and new requests correctly', async () => { + mockServerEventContext.setIsConnected(true); + + // Pre-populate cache with one item + dataCache.set('item-1', { id: 'item-1', name: 'Cached Item 1' }); + + const requestedIdBatches: Array> = []; + mockGetItems = async (ids: Array) => { + requestedIdBatches.push([...ids]); + await new Promise((resolve) => setTimeout(resolve, 50)); + return { data: ids.map((id) => ({ id, name: `Fresh Item ${id}` })) }; + }; + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Wait for context observation + await new Promise((resolve) => setTimeout(resolve, 10)); + + const [result1, result2] = await Promise.all([ + manager.getItems(['item-1', 'item-2', 'item-3']), + manager.getItems(['item-1', 'item-2', 'item-4']), + ]); + + // item-1 should come from data cache (not requested) + // item-2 and item-3 should be requested by first call + // item-4 should be requested by second call + // item-2 should NOT be re-requested by second call (inflight from first) + const allRequestedIds = requestedIdBatches.flat(); + expect(allRequestedIds).to.not.include('item-1'); // from cache + expect(allRequestedIds.filter((id) => id === 'item-2')).to.have.lengthOf(1); // only once + expect(allRequestedIds).to.include('item-3'); + expect(allRequestedIds).to.include('item-4'); + + // Both results should have all their requested items + expect(result1.data).to.have.lengthOf(3); + expect(result2.data).to.have.lengthOf(3); + + // Verify cached item was returned from cache with its cached name + const cachedItem = result1.data?.find((item) => item.id === 'item-1'); + expect(cachedItem?.name).to.equal('Cached Item 1'); + }); + }); + + describe('Server Event Connection', () => { + it('clears the cache when server connection is lost', async () => { + mockServerEventContext.setIsConnected(true); + + // Pre-populate cache + dataCache.set('item-1', { id: 'item-1', name: 'Cached Item 1' }); + dataCache.set('item-2', { id: 'item-2', name: 'Cached Item 2' }); + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Wait for context observation + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Verify items are in cache + expect(dataCache.has('item-1')).to.be.true; + expect(dataCache.has('item-2')).to.be.true; + + // Simulate losing connection + mockServerEventContext.setIsConnected(false); + + // Wait for observation to process + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Cache should be cleared + expect(dataCache.has('item-1')).to.be.false; + expect(dataCache.has('item-2')).to.be.false; + }); + + it('ignores undefined connection state', async () => { + // Pre-populate cache + dataCache.set('item-1', { id: 'item-1', name: 'Cached Item 1' }); + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + inflightRequestCache, + getUniqueMethod: (item) => item.id, + }); + + // Wait for context observation + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Cache should not be affected by undefined state + expect(dataCache.has('item-1')).to.be.true; + }); + }); + + describe('without inflight cache', () => { + it('works without an inflight cache (backwards compatibility)', async () => { + let requestedIds: Array = []; + mockGetItems = async (ids: Array) => { + requestedIds = ids; + return { data: ids.map((id) => ({ id, name: `Item ${id}` })) }; + }; + + manager = new UmbManagementApiItemDataRequestManager(hostElement, { + getItems: mockGetItems, + dataCache, + getUniqueMethod: (item) => item.id, + }); + + const result = await manager.getItems(['item-1', 'item-2']); + + expect(requestedIds).to.deep.equal(['item-1', 'item-2']); + expect(result.data).to.have.lengthOf(2); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.ts index 8e071a1b96ba..d0236a030baa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-data.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/management-api/item/item-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 { UmbManagementApiItemDataCache } from './cache.js'; import type { UmbApiError, UmbCancelError, UmbApiResponse } from '@umbraco-cms/backoffice/resources'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; @@ -8,11 +9,13 @@ import { UmbItemDataApiGetRequestController } from '@umbraco-cms/backoffice/enti export interface UmbManagementApiItemDataRequestManagerArgs { getItems: (unique: Array) => Promise }>>; dataCache: UmbManagementApiItemDataCache; + inflightRequestCache?: UmbManagementApiInFlightRequestCache; getUniqueMethod: (item: ItemResponseModelType) => string; } export class UmbManagementApiItemDataRequestManager extends UmbControllerBase { #dataCache: UmbManagementApiItemDataCache; + #inflightRequestCache?: UmbManagementApiInFlightRequestCache; #serverEventContext?: typeof UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT.TYPE; getUniqueMethod: (item: ItemResponseModelType) => string; @@ -24,6 +27,7 @@ export class UmbManagementApiItemDataRequestManager exten this.#getItems = args.getItems; this.#dataCache = args.dataCache; + this.#inflightRequestCache = args.inflightRequestCache; this.getUniqueMethod = args.getUniqueMethod; this.consumeContext(UMB_MANAGEMENT_API_SERVER_EVENT_CONTEXT, (context) => { @@ -36,7 +40,6 @@ export class UmbManagementApiItemDataRequestManager exten let error: UmbApiError | UmbCancelError | undefined; let idsToRequest: Array = [...ids]; let cacheItems: Array = []; - let serverItems: Array | undefined; // Only read from the cache when we are connected to the server events if (this.#isConnectedToServerEvents) { @@ -47,24 +50,89 @@ export class UmbManagementApiItemDataRequestManager exten idsToRequest = ids.filter((id) => !this.#dataCache.has(id)); } - if (idsToRequest.length > 0) { - const getItemsController = new UmbItemDataApiGetRequestController(this, { - api: (args) => this.#getItems(args.uniques), - uniques: idsToRequest, + // Split remaining IDs into those already inflight vs those needing a new request + const inflightPromises: Array>> = []; + const newIds: Array = []; + + for (const id of idsToRequest) { + const inflightCacheKey = `item:${id}`; + if (this.#inflightRequestCache?.has(inflightCacheKey)) { + inflightPromises.push(this.#inflightRequestCache.get(inflightCacheKey)!.requestPromise); + } else { + newIds.push(id); + } + } + + // For new IDs, create per-item deferred promises and store in inflight cache before making the API call + const deferredMap = new Map< + string, + { resolve: (value: UmbApiResponse<{ data?: ItemResponseModelType }>) => void } + >(); + + for (const id of newIds) { + const inflightCacheKey = `item:${id}`; + let resolve!: (value: UmbApiResponse<{ data?: ItemResponseModelType }>) => void; + const promise = new Promise>((r) => { + resolve = r; }); + deferredMap.set(id, { resolve }); + this.#inflightRequestCache?.set(inflightCacheKey, promise); + } + + // Fetch new IDs from the server + let newlyFetchedItems: Array = []; + + if (newIds.length > 0) { + try { + const getItemsController = new UmbItemDataApiGetRequestController(this, { + api: (args) => this.#getItems(args.uniques), + uniques: newIds, + }); + + const { data: serverData, error: serverError } = await getItemsController.request(); + const serverItems = serverData ?? []; + error = serverError; + + if (this.#isConnectedToServerEvents) { + serverItems.forEach((item) => this.#dataCache.set(this.getUniqueMethod(item), item)); + } + + newlyFetchedItems = serverItems; + + // Resolve each deferred promise with the corresponding item + for (const id of newIds) { + const item = serverItems.find((serverItem) => this.getUniqueMethod(serverItem) === id); + deferredMap.get(id)!.resolve({ data: item, error: serverError }); + } + } catch (e) { + // If the batch call throws, resolve all deferred promises with the error + for (const id of newIds) { + deferredMap.get(id)!.resolve({ data: undefined, error: e as UmbApiError | UmbCancelError }); + } + } finally { + // Always clean up inflight cache entries + for (const id of newIds) { + this.#inflightRequestCache?.delete(`item:${id}`); + } + } + } - const { data: serverData, error: serverError } = await getItemsController.request(); + // Await inflight results from other concurrent getItems calls + let inflightItems: Array = []; - serverItems = serverData ?? []; - error = serverError; + if (inflightPromises.length > 0) { + const inflightResults = await Promise.all(inflightPromises); + inflightItems = inflightResults + .map((result) => result.data) + .filter((x): x is ItemResponseModelType => x !== undefined); - if (this.#isConnectedToServerEvents) { - // If we are connected to server events, we can cache the server data - serverItems?.forEach((item) => this.#dataCache.set(this.getUniqueMethod(item), item)); + // Propagate the first inflight error if we don't already have one from our own batch + if (!error) { + error = inflightResults.find((result) => result.error)?.error; } } - const data: Array = [...cacheItems, ...(serverItems ?? [])]; + const data: Array = [...cacheItems, ...inflightItems, ...newlyFetchedItems]; return { data, error }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.request-manager.ts index d29c94880e28..ff623a7764bf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { mediaTypeItemCache } from './media-type-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { MediaTypeService, type MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiMediaTypeItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => MediaTypeService.getItemMediaType({ query: { id: ids } }), dataCache: mediaTypeItemCache, + inflightRequestCache: UmbManagementApiMediaTypeItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.request-manager.ts index 785e3d591508..f943362df4d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { mediaItemCache } from './media-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { MediaService, type MediaItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiMediaItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => MediaService.getItemMedia({ query: { id: ids } }), dataCache: mediaItemCache, + inflightRequestCache: UmbManagementApiMediaItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.request-manager.ts index c985f054a827..4922c099b401 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/member-group-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { memberGroupItemCache } from './member-group-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { MemberGroupService, type MemberGroupItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiMemberGroupItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => MemberGroupService.getItemMemberGroup({ query: { id: ids } }), dataCache: memberGroupItemCache, + inflightRequestCache: UmbManagementApiMemberGroupItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.request-manager.ts index c8c4d1a3ff60..aafcc602caf4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/item/member-type-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { memberTypeItemCache } from './member-type-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { MemberTypeService, type MemberTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiMemberTypeItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => MemberTypeService.getItemMemberType({ query: { id: ids } }), dataCache: memberTypeItemCache, + inflightRequestCache: UmbManagementApiMemberTypeItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.request-manager.ts index bbda9b60c125..12ea619fcd21 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/item/repository/member-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { memberItemCache } from './member-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { MemberService, type MemberItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiMemberItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => MemberService.getItemMember({ query: { id: ids } }), dataCache: memberItemCache, + inflightRequestCache: UmbManagementApiMemberItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.request-manager.ts index 567dec2d981d..f45c5d672ae3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { staticFileItemCache } from './static-file-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { StaticFileService, type StaticFileItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiStaticFileItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (paths: Array) => StaticFileService.getItemStaticFile({ query: { path: paths } }), dataCache: staticFileItemCache, + inflightRequestCache: UmbManagementApiStaticFileItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.path, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/repository/item/partial-view-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/repository/item/partial-view-item.server.request-manager.ts index d8e2026b2169..cb5e8912fd1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/repository/item/partial-view-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/repository/item/partial-view-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { partialViewItemCache } from './partial-view-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { PartialViewService, type PartialViewItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiPartialViewItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (paths: Array) => PartialViewService.getItemPartialView({ query: { path: paths } }), dataCache: partialViewItemCache, + inflightRequestCache: UmbManagementApiPartialViewItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.path, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/repository/item/script-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/repository/item/script-item.server.request-manager.ts index c99cf6fe7c89..d8e2e120381e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/repository/item/script-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/repository/item/script-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { scriptItemCache } from './script-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { ScriptService, type ScriptItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiScriptItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (paths: Array) => ScriptService.getItemScript({ query: { path: paths } }), dataCache: scriptItemCache, + inflightRequestCache: UmbManagementApiScriptItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.path, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.request-manager.ts index bf7f65e73bda..033701867f81 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { stylesheetItemCache } from './stylesheet-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { StylesheetService, type StylesheetItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiStylesheetItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (paths: Array) => StylesheetService.getItemStylesheet({ query: { path: paths } }), dataCache: stylesheetItemCache, + inflightRequestCache: UmbManagementApiStylesheetItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.path, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.request-manager.ts index 9d6e692987b5..e98750cff098 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/item/template-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { templateItemCache } from './template-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { TemplateService, type TemplateItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiTemplateItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => TemplateService.getItemTemplate({ query: { id: ids } }), dataCache: templateItemCache, + inflightRequestCache: UmbManagementApiTemplateItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.request-manager.ts index 2e6e50cb4818..4f7c7bd55c58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { userGroupItemCache } from './user-group-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UserGroupService, type UserGroupItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiUserGroupItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => UserGroupService.getItemUserGroup({ query: { id: ids } }), dataCache: userGroupItemCache, + inflightRequestCache: UmbManagementApiUserGroupItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.request-manager.ts index a2ee026149a6..3adaeb206039 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/item/user-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { userItemCache } from './user-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UserService, type UserItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiUserItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => UserService.getItemUser({ query: { id: ids } }), dataCache: userItemCache, + inflightRequestCache: UmbManagementApiUserItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.request-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.request-manager.ts index 489ffedef600..29bd6c5aab5c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.request-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.request-manager.ts @@ -2,13 +2,19 @@ import { webhookItemCache } from './webhook-item.server.cache.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { WebhookService, type WebhookItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbManagementApiItemDataRequestManager } from '@umbraco-cms/backoffice/management-api'; +import { + UmbManagementApiItemDataRequestManager, + UmbManagementApiInFlightRequestCache, +} from '@umbraco-cms/backoffice/management-api'; export class UmbManagementApiWebhookItemDataRequestManager extends UmbManagementApiItemDataRequestManager { + static #inflightRequestCache = new UmbManagementApiInFlightRequestCache(); + constructor(host: UmbControllerHost) { super(host, { getItems: (ids: Array) => WebhookService.getItemWebhook({ query: { id: ids } }), dataCache: webhookItemCache, + inflightRequestCache: UmbManagementApiWebhookItemDataRequestManager.#inflightRequestCache, getUniqueMethod: (item) => item.id, }); }