Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
73 changes: 56 additions & 17 deletions src/classes/CodDicomWebServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { CustomErrorEvent } from './customClasses';
import { download, getDirectoryHandle } from '../fileAccessSystemUtils';

class CodDicomWebServer {
private filePromises: Record<string, Promise<void>> = {};
private filePromises: Record<string, { promise: Promise<void>; requestCount: number }> = {};
private files: Record<string, Uint8Array> = {};
private options: CodDicomWebServerOptions = {
maxCacheSize: 4 * 1024 * 1024 * 1024, // 4GB
domain: constants.url.DOMAIN,
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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<ArrayBufferLike | undefined>((resolveRequest, rejectRequest) => {
let requestResolved = false;
let requestResolved = false,
fileFetchingCompleted = false;

const handleChunkAppend = (evt: CustomMessageEvent | CustomErrorEvent): void => {
if (evt instanceof CustomErrorEvent) {
Expand All @@ -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);
}
Expand All @@ -327,38 +340,64 @@ 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`));
}
}
})
.catch((error) => {
fileFetchingCompleted = true;
rejectRequest(error);
})
.then(() => {
dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend);
.finally(() => {
completeRequest(fileUrl);
});
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/dataRetrieval/dataRetrievalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class DataRetrievalManager {
this.dataRetriever.register(name, arg);
}

public async executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<void> {
public async executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<any> {
return await this.dataRetriever.executeTask(loaderName, taskName, options);
}

Expand All @@ -69,6 +69,7 @@ class DataRetrievalManager {
}

const dataRetrievalManager = new DataRetrievalManager();
Object.freeze(dataRetrievalManager);

export function getDataRetrievalManager(): DataRetrievalManager {
return dataRetrievalManager;
Expand Down
2 changes: 1 addition & 1 deletion src/dataRetrieval/requestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class RequestManager {
}
};

public async executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<void> {
public async executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<any> {
const loaderObject = this.loaderRegistry[loaderName]?.loaderObject;
if (!loaderObject) {
throw new CustomError(`Loader ${loaderName} not registered`);
Expand Down
14 changes: 9 additions & 5 deletions src/dataRetrieval/scripts/filePartial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const filePartial = {
directoryHandle?: FileSystemDirectoryHandle;
},
callBack: (data: { url: string; fileArraybuffer: Uint8Array; offsets: { startByte: number; endByte: number } }) => void
): Promise<void | Error> {
): Promise<Uint8Array | Error> {
const { url, offsets, headers, directoryHandle } = args;
if (offsets?.startByte && offsets?.endByte) {
headers['Range'] = `bytes=${offsets.startByte}-${offsets.endByte - 1}`;
Expand All @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions src/dataRetrieval/scripts/fileStreaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion src/dataRetrieval/workerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class WebWorkerManager {
}
}

public async executeTask(workerName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<void> {
public async executeTask(workerName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<any> {
const worker = this.workerRegistry[workerName]?.instance;
if (!worker) {
throw new CustomError(`Worker ${workerName} not registered`);
Expand Down
2 changes: 1 addition & 1 deletion src/tests/classes/CodDicomWebServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/tests/dataRetrieval/scripts/filePartial.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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);
});
Expand Down
18 changes: 18 additions & 0 deletions src/tests/fileManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading