From 339a953fc00af9ad678deaf88f3a75d1b1a4175c Mon Sep 17 00:00:00 2001 From: Adithyan Date: Fri, 2 Jan 2026 17:54:57 +0530 Subject: [PATCH] Avoid Cod dicomweb Server cache if OPFS is enabled --- package.json | 2 +- src/classes/CodDicomWebServer.ts | 73 ++++++++++++++----- src/dataRetrieval/dataRetrievalManager.ts | 3 +- src/dataRetrieval/requestManager.ts | 2 +- src/dataRetrieval/scripts/filePartial.ts | 14 ++-- src/dataRetrieval/scripts/fileStreaming.ts | 7 +- src/dataRetrieval/workerManager.ts | 2 +- src/tests/classes/CodDicomWebServer.test.ts | 2 +- .../dataRetrieval/scripts/filePartial.test.ts | 4 +- src/tests/fileManager.test.ts | 18 +++++ 10 files changed, 96 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 52fb3d1..1fd0f71 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cod-dicomweb-server", "title": "COD Dicomweb server", - "version": "1.3.12", + "version": "1.3.13", "private": false, "description": "A wadors server proxy that get data from a Cloud Optimized Dicom format.", "main": "dist/umd/main.js", diff --git a/src/classes/CodDicomWebServer.ts b/src/classes/CodDicomWebServer.ts index 2144a98..c84b6d8 100644 --- a/src/classes/CodDicomWebServer.ts +++ b/src/classes/CodDicomWebServer.ts @@ -19,7 +19,8 @@ import { CustomErrorEvent } from './customClasses'; import { download, getDirectoryHandle } from '../fileAccessSystemUtils'; class CodDicomWebServer { - private filePromises: Record> = {}; + private filePromises: Record; requestCount: number }> = {}; + private files: Record = {}; private options: CodDicomWebServerOptions = { maxCacheSize: 4 * 1024 * 1024 * 1024, // 4GB domain: constants.url.DOMAIN, @@ -231,7 +232,11 @@ class CodDicomWebServer { const { url, position, fileArraybuffer } = evt.data; if (url === fileUrl && fileArraybuffer) { - this.fileManager.set(url, { data: fileArraybuffer, position }); + if (this.options.enableLocalCache) { + this.files[fileUrl] = fileArraybuffer; + } else { + this.fileManager.set(url, { data: fileArraybuffer, position }); + } dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk); } @@ -253,7 +258,6 @@ class CodDicomWebServer { }) .then(() => { dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk); - delete this.filePromises[fileUrl]; }); } else if (fetchType === FetchTypeEnum.BYTES_OPTIMIZED && offsets) { const { startByte, endByte } = offsets; @@ -268,7 +272,11 @@ class CodDicomWebServer { const { url, fileArraybuffer, offsets } = evt.data; if (url === bytesRemovedUrl && offsets.startByte === startByte && offsets.endByte === endByte) { - this.fileManager.set(fileUrl, { data: fileArraybuffer, position: fileArraybuffer.length }); + if (this.options.enableLocalCache) { + this.files[fileUrl] = fileArraybuffer; + } else { + this.fileManager.set(fileUrl, { data: fileArraybuffer, position: fileArraybuffer.length }); + } dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice); resolveFile(); @@ -289,20 +297,21 @@ class CodDicomWebServer { }) .then(() => { dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice); - delete this.filePromises[fileUrl]; }); } else { rejectFile(new CustomError('CodDicomWebServer.ts: Offsets is needed in bytes optimized fetching')); } }); - this.filePromises[fileUrl] = tarPromise; + this.filePromises[fileUrl] = { promise: tarPromise, requestCount: 1 }; } else { - tarPromise = this.filePromises[fileUrl]; + tarPromise = this.filePromises[fileUrl].promise; + this.filePromises[fileUrl].requestCount++; } return new Promise((resolveRequest, rejectRequest) => { - let requestResolved = false; + let requestResolved = false, + fileFetchingCompleted = false; const handleChunkAppend = (evt: CustomMessageEvent | CustomErrorEvent): void => { if (evt instanceof CustomErrorEvent) { @@ -314,7 +323,11 @@ class CodDicomWebServer { if (isAppending) { if (chunk) { - this.fileManager.append(url, chunk, position); + if (this.options.enableLocalCache) { + this.files[url].set(chunk, position - chunk.length); + } else { + this.fileManager.append(url, chunk, position); + } } else { this.fileManager.setPosition(url, position); } @@ -327,27 +340,52 @@ class CodDicomWebServer { } } - if (!requestResolved && url === fileUrl && offsets && position > offsets.endByte) { + if (!requestResolved && url === fileUrl && position > offsets.endByte) { try { - const file = this.fileManager.get(url, offsets); - requestResolved = true; + const file = this.options.enableLocalCache + ? this.files[url].slice(offsets.startByte, offsets.endByte) + : this.fileManager.get(url, offsets); + resolveRequest(file?.buffer); } catch (error) { rejectRequest(error); + } finally { + completeRequest(url); } } }; + const completeRequest = (url: string) => { + requestResolved = true; + this.filePromises[url].requestCount--; + dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend); + + if (fileFetchingCompleted && this.filePromises[url] && !this.filePromises[url]?.requestCount) { + delete this.filePromises[url]; + delete this.files[url]; + } + }; + if (offsets && !isBytesOptimized) { dataRetrievalManager.addEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend); } tarPromise .then(() => { + fileFetchingCompleted = true; + if (!requestResolved) { - if (this.fileManager.getPosition(fileUrl)) { - const file = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets); - requestResolved = true; + if (this.fileManager.getPosition(fileUrl) || this.files[fileUrl]) { + let file: Uint8Array; + if (this.options.enableLocalCache) { + file = + isBytesOptimized || !offsets + ? this.files[fileUrl] + : this.files[fileUrl].slice(offsets.startByte, offsets.endByte); + } else { + file = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets); + } + resolveRequest(file?.buffer); } else { rejectRequest(new CustomError(`File - ${fileUrl} not found`)); @@ -355,10 +393,11 @@ class CodDicomWebServer { } }) .catch((error) => { + fileFetchingCompleted = true; rejectRequest(error); }) - .then(() => { - dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend); + .finally(() => { + completeRequest(fileUrl); }); }); } diff --git a/src/dataRetrieval/dataRetrievalManager.ts b/src/dataRetrieval/dataRetrievalManager.ts index 5502d94..8e50376 100644 --- a/src/dataRetrieval/dataRetrievalManager.ts +++ b/src/dataRetrieval/dataRetrievalManager.ts @@ -43,7 +43,7 @@ class DataRetrievalManager { this.dataRetriever.register(name, arg); } - public async executeTask(loaderName: string, taskName: string, options: Record | unknown): Promise { + public async executeTask(loaderName: string, taskName: string, options: Record | unknown): Promise { return await this.dataRetriever.executeTask(loaderName, taskName, options); } @@ -69,6 +69,7 @@ class DataRetrievalManager { } const dataRetrievalManager = new DataRetrievalManager(); +Object.freeze(dataRetrievalManager); export function getDataRetrievalManager(): DataRetrievalManager { return dataRetrievalManager; diff --git a/src/dataRetrieval/requestManager.ts b/src/dataRetrieval/requestManager.ts index 180b22a..7eb6796 100644 --- a/src/dataRetrieval/requestManager.ts +++ b/src/dataRetrieval/requestManager.ts @@ -34,7 +34,7 @@ class RequestManager { } }; - public async executeTask(loaderName: string, taskName: string, options: Record | unknown): Promise { + public async executeTask(loaderName: string, taskName: string, options: Record | unknown): Promise { const loaderObject = this.loaderRegistry[loaderName]?.loaderObject; if (!loaderObject) { throw new CustomError(`Loader ${loaderName} not registered`); diff --git a/src/dataRetrieval/scripts/filePartial.ts b/src/dataRetrieval/scripts/filePartial.ts index 13367db..9ee89ec 100644 --- a/src/dataRetrieval/scripts/filePartial.ts +++ b/src/dataRetrieval/scripts/filePartial.ts @@ -10,7 +10,7 @@ const filePartial = { directoryHandle?: FileSystemDirectoryHandle; }, callBack: (data: { url: string; fileArraybuffer: Uint8Array; offsets: { startByte: number; endByte: number } }) => void - ): Promise { + ): Promise { const { url, offsets, headers, directoryHandle } = args; if (offsets?.startByte && offsets?.endByte) { headers['Range'] = `bytes=${offsets.startByte}-${offsets.endByte - 1}`; @@ -21,19 +21,23 @@ const filePartial = { if (directoryHandle) { const file = (await readFile(directoryHandle, storageName, { offsets, isJson: false })) as ArrayBuffer; if (file) { - callBack({ url, fileArraybuffer: new Uint8Array(file), offsets }); - return; + const fileBuffer = new Uint8Array(file); + callBack({ url, fileArraybuffer: fileBuffer, offsets }); + return fileBuffer; } } - await fetch(url, { headers }) + return await fetch(url, { headers }) .then((response) => response.arrayBuffer()) .then((data) => { - callBack({ url, fileArraybuffer: new Uint8Array(data), offsets }); + const fileBuffer = new Uint8Array(data); + callBack({ url, fileArraybuffer: fileBuffer, offsets }); if (directoryHandle) { writeFile(directoryHandle, storageName, data); } + + return fileBuffer; }) .catch((error) => { throw new CustomError('filePartial.ts: Error when fetching file: ' + error?.message); diff --git a/src/dataRetrieval/scripts/fileStreaming.ts b/src/dataRetrieval/scripts/fileStreaming.ts index 1253907..2bcd752 100644 --- a/src/dataRetrieval/scripts/fileStreaming.ts +++ b/src/dataRetrieval/scripts/fileStreaming.ts @@ -29,8 +29,9 @@ const fileStreaming = { const file = (await readFile(directoryHandle, fileName, { isJson: false })) as ArrayBuffer; if (file) { const totalLength = file.byteLength; - callBack({ url, position: totalLength, fileArraybuffer: new Uint8Array(file), totalLength }); - return; + const fileBuffer = new Uint8Array(file); + callBack({ url, position: totalLength, fileArraybuffer: fileBuffer, totalLength }); + return fileBuffer; } } @@ -96,6 +97,8 @@ const fileStreaming = { writeFile(directoryHandle, fileName, fileArraybuffer.slice().buffer); } } + + return fileArraybuffer; } catch (error) { const streamingError = new CustomError( 'fileStreaming.ts: ' + (error as CustomError).message || 'An error occured when streaming' diff --git a/src/dataRetrieval/workerManager.ts b/src/dataRetrieval/workerManager.ts index f7005f2..b359c16 100644 --- a/src/dataRetrieval/workerManager.ts +++ b/src/dataRetrieval/workerManager.ts @@ -21,7 +21,7 @@ class WebWorkerManager { } } - public async executeTask(workerName: string, taskName: string, options: Record | unknown): Promise { + public async executeTask(workerName: string, taskName: string, options: Record | unknown): Promise { const worker = this.workerRegistry[workerName]?.instance; if (!worker) { throw new CustomError(`Worker ${workerName} not registered`); diff --git a/src/tests/classes/CodDicomWebServer.test.ts b/src/tests/classes/CodDicomWebServer.test.ts index 5cad960..672681d 100644 --- a/src/tests/classes/CodDicomWebServer.test.ts +++ b/src/tests/classes/CodDicomWebServer.test.ts @@ -76,7 +76,7 @@ describe('CodDicomWebServer', () => { describe('getOptions', () => { it('should return the default options if not set', () => { const options = server.getOptions(); - expect(options).toEqual({ maxCacheSize: Infinity, domain: url.DOMAIN, enableLocalCache: false }); + expect(options).toEqual({ maxCacheSize: 4 * 1024 * 1024 * 1024, domain: url.DOMAIN, enableLocalCache: false }); }); }); diff --git a/src/tests/dataRetrieval/scripts/filePartial.test.ts b/src/tests/dataRetrieval/scripts/filePartial.test.ts index 0dc17c8..9b0a087 100644 --- a/src/tests/dataRetrieval/scripts/filePartial.test.ts +++ b/src/tests/dataRetrieval/scripts/filePartial.test.ts @@ -76,7 +76,7 @@ describe('filePartial', () => { const headers = { 'Content-Type': 'application/json' }; const callback = jest.fn(); const expected = await (await URL_RESPONSES[url]).arrayBuffer(); - await expect(filePartial.partial({ url, offsets, headers }, callback)).resolves.toBeUndefined(); + await expect(filePartial.partial({ url, offsets, headers }, callback)).resolves.toEqual(expected); expect(callback).toHaveBeenCalledWith({ url, fileArraybuffer: new Uint8Array(expected), offsets }); expect(callback).toHaveBeenCalledTimes(1); }); @@ -87,7 +87,7 @@ describe('filePartial', () => { const headers = { 'Content-Type': 'application/json' }; const callback = jest.fn(); const expected = (await (await URL_RESPONSES[url]).arrayBuffer()).slice(3, 7); - await expect(filePartial.partial({ url, offsets, headers }, callback)).resolves.toBeUndefined(); + await expect(filePartial.partial({ url, offsets, headers }, callback)).resolves.toEqual(expected); expect(callback).toHaveBeenCalledWith({ url, fileArraybuffer: new Uint8Array(expected), offsets }); expect(callback).toHaveBeenCalledTimes(1); }); diff --git a/src/tests/fileManager.test.ts b/src/tests/fileManager.test.ts index 08fb850..3b40766 100644 --- a/src/tests/fileManager.test.ts +++ b/src/tests/fileManager.test.ts @@ -148,4 +148,22 @@ describe('FileManager', () => { expect(fileManager.get(url2)).toBeNull(); expect(fileManager.getTotalSize()).toEqual(0); }); + + it('should decache the necessary bytes', () => { + const url1 = 'test-url-1'; + const file1 = { data: new Uint8Array([1, 2, 3]), position: 3 }; + fileManager.set(url1, file1); + const url2 = 'test-url-2'; + const file2 = { data: new Uint8Array([4, 5, 6]), position: 3 }; + fileManager.set(url2, file2); + const url3 = 'test-url-3'; + const file3 = { data: new Uint8Array([7, 8]), position: 2 }; + fileManager.set(url3, file3); + fileManager.decacheNecessaryBytes(url3, file3.data.byteLength); + + expect(fileManager.get(url1)).toBeNull(); + expect(fileManager.get(url2)).toBe(file2.data); + expect(fileManager.get(url3)).toBe(file3.data); + expect(fileManager.getTotalSize()).toEqual(5); + }); });