cod-dicomweb-server 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +15 -0
  2. package/dist/cjs/1104a37b16dee0d2ada1.ts +14 -0
  3. package/dist/cjs/7d4e5892d21def245792.ts +14 -0
  4. package/dist/cjs/main.js +1957 -0
  5. package/dist/{types → esm}/classes/CodDicomWebServer.d.ts +2 -1
  6. package/dist/esm/classes/CodDicomWebServer.js +323 -0
  7. package/dist/esm/classes/customClasses.d.ts +19 -0
  8. package/dist/esm/classes/customClasses.js +13 -0
  9. package/dist/esm/classes/index.d.ts +2 -0
  10. package/dist/esm/classes/index.js +2 -0
  11. package/dist/esm/classes/utils.js +102 -0
  12. package/dist/esm/constants/dataRetrieval.js +3 -0
  13. package/dist/{types → esm}/constants/enums.d.ts +4 -0
  14. package/dist/esm/constants/enums.js +24 -0
  15. package/dist/{types → esm}/constants/index.d.ts +3 -3
  16. package/dist/esm/constants/index.js +6 -0
  17. package/dist/esm/constants/url.js +3 -0
  18. package/dist/esm/dataRetrieval/dataRetrievalManager.d.ts +17 -0
  19. package/dist/esm/dataRetrieval/dataRetrievalManager.js +54 -0
  20. package/dist/esm/dataRetrieval/register.d.ts +4 -0
  21. package/dist/esm/dataRetrieval/register.js +25 -0
  22. package/dist/esm/dataRetrieval/requestManager.d.ts +12 -0
  23. package/dist/esm/dataRetrieval/requestManager.js +65 -0
  24. package/dist/esm/dataRetrieval/scripts/filePartial.d.ts +18 -0
  25. package/dist/esm/dataRetrieval/scripts/filePartial.js +16 -0
  26. package/dist/{types/webWorker → esm/dataRetrieval}/scripts/fileStreaming.d.ts +7 -1
  27. package/dist/esm/dataRetrieval/scripts/fileStreaming.js +93 -0
  28. package/dist/esm/dataRetrieval/utils/environment.d.ts +1 -0
  29. package/dist/esm/dataRetrieval/utils/environment.js +3 -0
  30. package/dist/esm/dataRetrieval/workerManager.d.ts +10 -0
  31. package/dist/esm/dataRetrieval/workerManager.js +55 -0
  32. package/dist/esm/dataRetrieval/workers/filePartialWorker.js +7 -0
  33. package/dist/esm/dataRetrieval/workers/fileStreamingWorker.js +7 -0
  34. package/dist/{types → esm}/fileManager.d.ts +2 -2
  35. package/dist/esm/fileManager.js +52 -0
  36. package/dist/{types → esm}/index.d.ts +1 -1
  37. package/dist/esm/index.js +4 -0
  38. package/dist/{types → esm}/metadataManager.d.ts +1 -0
  39. package/dist/esm/metadataManager.js +47 -0
  40. package/dist/esm/types/codDicomWebServerOptions.js +1 -0
  41. package/dist/{types → esm}/types/fileManagerOptions.d.ts +1 -1
  42. package/dist/esm/types/fileManagerOptions.js +1 -0
  43. package/dist/{types → esm}/types/index.d.ts +1 -1
  44. package/dist/esm/types/index.js +7 -0
  45. package/dist/{types → esm}/types/metadata.d.ts +1 -1
  46. package/dist/esm/types/metadata.js +1 -0
  47. package/dist/esm/types/metadataUrlCreationParams.js +1 -0
  48. package/dist/esm/types/parsedWadoRsUrlDetails.js +1 -0
  49. package/dist/esm/types/requestOptions.js +1 -0
  50. package/dist/esm/types/scriptObject.d.ts +4 -0
  51. package/dist/esm/types/scriptObject.js +1 -0
  52. package/dist/umd/614.js +19 -0
  53. package/dist/umd/614.js.map +1 -0
  54. package/dist/umd/66.js +19 -0
  55. package/dist/umd/66.js.map +1 -0
  56. package/dist/umd/main.js +21 -0
  57. package/dist/umd/main.js.map +1 -0
  58. package/package.json +18 -6
  59. package/dist/16.js +0 -19
  60. package/dist/16.js.map +0 -1
  61. package/dist/170.js +0 -19
  62. package/dist/170.js.map +0 -1
  63. package/dist/main.js +0 -21
  64. package/dist/main.js.map +0 -1
  65. package/dist/types/types/workerCustomMessageEvents.d.ts +0 -10
  66. package/dist/types/webWorker/registerWorkers.d.ts +0 -4
  67. package/dist/types/webWorker/scripts/filePartial.d.ts +0 -7
  68. package/dist/types/webWorker/workerManager.d.ts +0 -10
  69. /package/dist/{types → esm}/classes/utils.d.ts +0 -0
  70. /package/dist/{types/constants/worker.d.ts → esm/constants/dataRetrieval.d.ts} +0 -0
  71. /package/dist/{types → esm}/constants/url.d.ts +0 -0
  72. /package/dist/{types/webWorker → esm/dataRetrieval}/workers/filePartialWorker.d.ts +0 -0
  73. /package/dist/{types/webWorker → esm/dataRetrieval}/workers/fileStreamingWorker.d.ts +0 -0
  74. /package/dist/{types → esm}/types/codDicomWebServerOptions.d.ts +0 -0
  75. /package/dist/{types → esm}/types/metadataUrlCreationParams.d.ts +0 -0
  76. /package/dist/{types → esm}/types/parsedWadoRsUrlDetails.d.ts +0 -0
  77. /package/dist/{types → esm}/types/requestOptions.d.ts +0 -0
@@ -1,5 +1,5 @@
1
1
  import { Enums } from '../constants';
2
- import type { CodDicomWebServerOptions, InstanceMetadata, JsonMetadata, SeriesMetadata, CODRequestOptions, FileRequestOptions } from '../types';
2
+ import type { CodDicomWebServerOptions, CODRequestOptions, FileRequestOptions, InstanceMetadata, JsonMetadata, SeriesMetadata } from '../types';
3
3
  declare class CodDicomWebServer {
4
4
  private filePromises;
5
5
  private options;
@@ -9,6 +9,7 @@ declare class CodDicomWebServer {
9
9
  constructor(args?: {
10
10
  maxWorkerFetchSize?: number;
11
11
  domain?: string;
12
+ disableWorker?: boolean;
12
13
  });
13
14
  setOptions: (newOptions: Partial<CodDicomWebServerOptions>) => void;
14
15
  getOptions: () => CodDicomWebServerOptions;
@@ -0,0 +1,323 @@
1
+ import { parseDicom } from 'dicom-parser';
2
+ import FileManager from '../fileManager';
3
+ import MetadataManager from '../metadataManager';
4
+ import { getFrameDetailsFromMetadata, parseWadorsURL } from './utils';
5
+ import { register } from '../dataRetrieval/register';
6
+ import constants, { Enums } from '../constants';
7
+ import { getDataRetrievalManager } from '../dataRetrieval/dataRetrievalManager';
8
+ import { CustomError } from './customClasses';
9
+ import { CustomErrorEvent } from './customClasses';
10
+ class CodDicomWebServer {
11
+ filePromises = {};
12
+ options = {
13
+ maxWorkerFetchSize: Infinity,
14
+ domain: constants.url.DOMAIN
15
+ };
16
+ fileManager;
17
+ metadataManager;
18
+ seriesUidFileUrls = {};
19
+ constructor(args = {}) {
20
+ const { maxWorkerFetchSize, domain, disableWorker } = args;
21
+ this.options.maxWorkerFetchSize = maxWorkerFetchSize || this.options.maxWorkerFetchSize;
22
+ this.options.domain = domain || this.options.domain;
23
+ const fileStreamingScriptName = constants.dataRetrieval.FILE_STREAMING_WORKER_NAME;
24
+ const filePartialScriptName = constants.dataRetrieval.FILE_PARTIAL_WORKER_NAME;
25
+ this.fileManager = new FileManager({ fileStreamingScriptName });
26
+ this.metadataManager = new MetadataManager();
27
+ if (disableWorker) {
28
+ const dataRetrievalManager = getDataRetrievalManager();
29
+ dataRetrievalManager.setDataRetrieverMode(Enums.DataRetrieveMode.REQUEST);
30
+ }
31
+ register({ fileStreamingScriptName, filePartialScriptName }, this.options.maxWorkerFetchSize);
32
+ }
33
+ setOptions = (newOptions) => {
34
+ Object.keys(newOptions).forEach((key) => {
35
+ if (newOptions[key] !== undefined) {
36
+ this.options[key] = newOptions[key];
37
+ }
38
+ });
39
+ };
40
+ getOptions = () => {
41
+ return this.options;
42
+ };
43
+ addFileUrl(seriesInstanceUID, url) {
44
+ if (this.seriesUidFileUrls[seriesInstanceUID]) {
45
+ this.seriesUidFileUrls[seriesInstanceUID].push(url);
46
+ }
47
+ else {
48
+ this.seriesUidFileUrls[seriesInstanceUID] = [url];
49
+ }
50
+ }
51
+ async fetchCod(wadorsUrl, headers = {}, { useSharedArrayBuffer = false, fetchType = constants.Enums.FetchType.API_OPTIMIZED } = {}) {
52
+ try {
53
+ if (!wadorsUrl) {
54
+ throw new CustomError('Url not provided');
55
+ }
56
+ const parsedDetails = parseWadorsURL(wadorsUrl, this.options.domain);
57
+ if (parsedDetails) {
58
+ const { type, bucketName, bucketPrefix, studyInstanceUID, seriesInstanceUID, sopInstanceUID, frameNumber } = parsedDetails;
59
+ const metadataJson = await this.metadataManager.getMetadata({
60
+ domain: this.options.domain,
61
+ bucketName,
62
+ bucketPrefix,
63
+ studyInstanceUID,
64
+ seriesInstanceUID
65
+ }, headers);
66
+ if (!metadataJson) {
67
+ throw new CustomError(`Metadata not found for ${wadorsUrl}`);
68
+ }
69
+ const { url: fileUrl, startByte, endByte, thumbnailUrl, isMultiframe } = getFrameDetailsFromMetadata(metadataJson, sopInstanceUID, frameNumber - 1, {
70
+ domain: this.options.domain,
71
+ bucketName,
72
+ bucketPrefix
73
+ });
74
+ switch (type) {
75
+ case Enums.RequestType.THUMBNAIL:
76
+ if (!thumbnailUrl) {
77
+ throw new CustomError(`Thumbnail not found for ${wadorsUrl}`);
78
+ }
79
+ this.addFileUrl(seriesInstanceUID, thumbnailUrl);
80
+ return this.fetchFile(thumbnailUrl, headers, {
81
+ useSharedArrayBuffer
82
+ });
83
+ case Enums.RequestType.FRAME: {
84
+ if (!fileUrl) {
85
+ throw new CustomError('Url not found for frame');
86
+ }
87
+ let urlWithBytes = fileUrl;
88
+ if (fetchType === Enums.FetchType.BYTES_OPTIMIZED) {
89
+ urlWithBytes = `${fileUrl}?bytes=${startByte}-${endByte}`;
90
+ }
91
+ this.addFileUrl(seriesInstanceUID, fileUrl);
92
+ return this.fetchFile(urlWithBytes, headers, {
93
+ offsets: { startByte, endByte },
94
+ useSharedArrayBuffer,
95
+ fetchType
96
+ }).then((arraybuffer) => {
97
+ if (!arraybuffer?.byteLength) {
98
+ throw new CustomError('File Arraybuffer is not found');
99
+ }
100
+ if (isMultiframe) {
101
+ return arraybuffer;
102
+ }
103
+ else {
104
+ const dataSet = parseDicom(new Uint8Array(arraybuffer));
105
+ const pixelDataElement = dataSet.elements.x7fe00010;
106
+ let { dataOffset, length } = pixelDataElement;
107
+ if (pixelDataElement.hadUndefinedLength && pixelDataElement.fragments) {
108
+ ({ position: dataOffset, length } = pixelDataElement.fragments[0]);
109
+ }
110
+ else {
111
+ // Adding 8 bytes for 4 bytes tag + 4 bytes length for uncomppressed pixelData
112
+ dataOffset += 8;
113
+ }
114
+ return arraybuffer.slice(dataOffset, dataOffset + length);
115
+ }
116
+ });
117
+ }
118
+ case Enums.RequestType.SERIES_METADATA:
119
+ case Enums.RequestType.INSTANCE_METADATA:
120
+ return this.parseMetadata(metadataJson, type, sopInstanceUID);
121
+ default:
122
+ throw new CustomError(`Unsupported request type: ${type}`);
123
+ }
124
+ }
125
+ else {
126
+ return new Promise((resolve, reject) => {
127
+ return this.fetchFile(wadorsUrl, headers, { useSharedArrayBuffer })
128
+ .then((result) => {
129
+ if (result instanceof ArrayBuffer) {
130
+ try {
131
+ const dataSet = parseDicom(new Uint8Array(result));
132
+ const seriesInstanceUID = dataSet.string('0020000e');
133
+ if (seriesInstanceUID) {
134
+ this.addFileUrl(seriesInstanceUID, wadorsUrl);
135
+ }
136
+ }
137
+ catch (error) {
138
+ console.warn('CodDicomWebServer.ts: There is some issue parsing the file.', error);
139
+ }
140
+ }
141
+ resolve(result);
142
+ })
143
+ .catch((error) => reject(error));
144
+ });
145
+ }
146
+ }
147
+ catch (error) {
148
+ const newError = new CustomError(`CodDicomWebServer.ts: ${error.message || 'An error occured when fetching the COD'}`);
149
+ console.error(newError);
150
+ throw newError;
151
+ }
152
+ }
153
+ async fetchFile(fileUrl, headers, { offsets, useSharedArrayBuffer = false, fetchType = constants.Enums.FetchType.API_OPTIMIZED } = {}) {
154
+ const isBytesOptimized = fetchType === Enums.FetchType.BYTES_OPTIMIZED;
155
+ const extractedFile = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets);
156
+ if (extractedFile) {
157
+ return new Promise((resolveRequest, rejectRequest) => {
158
+ try {
159
+ resolveRequest(extractedFile.buffer);
160
+ }
161
+ catch (error) {
162
+ rejectRequest(error);
163
+ }
164
+ });
165
+ }
166
+ const { maxWorkerFetchSize } = this.getOptions();
167
+ const dataRetrievalManager = getDataRetrievalManager();
168
+ const { FILE_STREAMING_WORKER_NAME, FILE_PARTIAL_WORKER_NAME, THRESHOLD } = constants.dataRetrieval;
169
+ let tarPromise;
170
+ if (!this.filePromises[fileUrl]) {
171
+ tarPromise = new Promise((resolveFile, rejectFile) => {
172
+ if (this.fileManager.getTotalSize() + THRESHOLD > maxWorkerFetchSize) {
173
+ throw new CustomError(`CodDicomWebServer.ts: Maximum size(${maxWorkerFetchSize}) for fetching files reached`);
174
+ }
175
+ const FetchTypeEnum = constants.Enums.FetchType;
176
+ if (fetchType === FetchTypeEnum.API_OPTIMIZED) {
177
+ const handleFirstChunk = (evt) => {
178
+ if (evt instanceof CustomErrorEvent) {
179
+ rejectFile(evt.error);
180
+ throw evt.error;
181
+ }
182
+ const { url, position, fileArraybuffer } = evt.data;
183
+ if (url === fileUrl && fileArraybuffer) {
184
+ this.fileManager.set(url, { data: fileArraybuffer, position });
185
+ dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk);
186
+ }
187
+ };
188
+ dataRetrievalManager.addEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk);
189
+ dataRetrievalManager
190
+ .executeTask(FILE_STREAMING_WORKER_NAME, 'stream', {
191
+ url: fileUrl,
192
+ headers: headers,
193
+ useSharedArrayBuffer
194
+ })
195
+ .then(() => {
196
+ resolveFile();
197
+ })
198
+ .catch((error) => {
199
+ rejectFile(error);
200
+ })
201
+ .then(() => {
202
+ dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleFirstChunk);
203
+ delete this.filePromises[fileUrl];
204
+ });
205
+ }
206
+ else if (fetchType === FetchTypeEnum.BYTES_OPTIMIZED && offsets) {
207
+ const { startByte, endByte } = offsets;
208
+ const bytesRemovedUrl = fileUrl.split('?bytes=')[0];
209
+ const handleSlice = (evt) => {
210
+ if (evt instanceof CustomErrorEvent) {
211
+ rejectFile(evt.error);
212
+ throw evt.error;
213
+ }
214
+ const { url, fileArraybuffer, offsets } = evt.data;
215
+ if (url === bytesRemovedUrl && offsets.startByte === startByte && offsets.endByte === endByte) {
216
+ this.fileManager.set(fileUrl, { data: fileArraybuffer, position: fileArraybuffer.length });
217
+ dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice);
218
+ resolveFile();
219
+ }
220
+ };
221
+ dataRetrievalManager.addEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice);
222
+ dataRetrievalManager
223
+ .executeTask(FILE_PARTIAL_WORKER_NAME, 'partial', {
224
+ url: bytesRemovedUrl,
225
+ offsets: { startByte, endByte },
226
+ headers,
227
+ useSharedArrayBuffer
228
+ })
229
+ .catch((error) => {
230
+ rejectFile(error);
231
+ })
232
+ .then(() => {
233
+ dataRetrievalManager.removeEventListener(FILE_PARTIAL_WORKER_NAME, 'message', handleSlice);
234
+ delete this.filePromises[fileUrl];
235
+ });
236
+ }
237
+ else {
238
+ rejectFile(new CustomError('CodDicomWebServer.ts: Offsets is needed in bytes optimized fetching'));
239
+ }
240
+ });
241
+ this.filePromises[fileUrl] = tarPromise;
242
+ }
243
+ else {
244
+ tarPromise = this.filePromises[fileUrl];
245
+ }
246
+ return new Promise((resolveRequest, rejectRequest) => {
247
+ let requestResolved = false;
248
+ const handleChunkAppend = (evt) => {
249
+ if (evt instanceof CustomErrorEvent) {
250
+ rejectRequest(evt.message);
251
+ throw evt.error;
252
+ }
253
+ const { url, position, chunk, isAppending } = evt.data;
254
+ if (isAppending) {
255
+ if (chunk) {
256
+ this.fileManager.append(url, chunk, position);
257
+ }
258
+ else {
259
+ this.fileManager.setPosition(url, position);
260
+ }
261
+ }
262
+ if (!requestResolved && url === fileUrl && offsets && position > offsets.endByte) {
263
+ try {
264
+ const file = this.fileManager.get(url, offsets);
265
+ requestResolved = true;
266
+ resolveRequest(file?.buffer);
267
+ }
268
+ catch (error) {
269
+ rejectRequest(error);
270
+ }
271
+ }
272
+ };
273
+ if (offsets && !isBytesOptimized) {
274
+ dataRetrievalManager.addEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend);
275
+ }
276
+ tarPromise
277
+ .then(() => {
278
+ if (!requestResolved) {
279
+ if (this.fileManager.getPosition(fileUrl)) {
280
+ const file = this.fileManager.get(fileUrl, isBytesOptimized ? undefined : offsets);
281
+ requestResolved = true;
282
+ resolveRequest(file?.buffer);
283
+ }
284
+ else {
285
+ rejectRequest(new CustomError(`File - ${fileUrl} not found`));
286
+ }
287
+ }
288
+ })
289
+ .catch((error) => {
290
+ rejectRequest(error);
291
+ })
292
+ .then(() => {
293
+ dataRetrievalManager.removeEventListener(FILE_STREAMING_WORKER_NAME, 'message', handleChunkAppend);
294
+ });
295
+ });
296
+ }
297
+ delete(seriesInstanceUID) {
298
+ const fileUrls = this.seriesUidFileUrls[seriesInstanceUID];
299
+ if (fileUrls) {
300
+ fileUrls.forEach((fileUrl) => {
301
+ this.fileManager.remove(fileUrl);
302
+ });
303
+ }
304
+ delete this.seriesUidFileUrls[seriesInstanceUID];
305
+ }
306
+ deleteAll() {
307
+ Object.values(this.seriesUidFileUrls).forEach((fileUrls) => {
308
+ fileUrls.forEach((fileUrl) => {
309
+ this.fileManager.remove(fileUrl);
310
+ });
311
+ });
312
+ this.seriesUidFileUrls = {};
313
+ }
314
+ parseMetadata(metadata, type, sopInstanceUID) {
315
+ if (type === Enums.RequestType.INSTANCE_METADATA) {
316
+ return Object.values(metadata.cod.instances).find((instance) => instance.metadata['00080018']?.Value?.[0] === sopInstanceUID)?.metadata;
317
+ }
318
+ else {
319
+ return Object.values(metadata.cod.instances).map((instance) => instance.metadata);
320
+ }
321
+ }
322
+ }
323
+ export default CodDicomWebServer;
@@ -0,0 +1,19 @@
1
+ export declare class CustomError extends Error {
2
+ }
3
+ export declare class CustomErrorEvent extends Event {
4
+ error: CustomError;
5
+ message: string;
6
+ constructor(message: string, error: CustomError);
7
+ }
8
+ export declare class CustomMessageEvent extends MessageEvent<{
9
+ url: string;
10
+ position: number;
11
+ chunk?: Uint8Array;
12
+ isAppending?: boolean;
13
+ fileArraybuffer?: Uint8Array;
14
+ offsets?: {
15
+ startByte: number;
16
+ endByte: number;
17
+ };
18
+ }> {
19
+ }
@@ -0,0 +1,13 @@
1
+ export class CustomError extends Error {
2
+ }
3
+ export class CustomErrorEvent extends Event {
4
+ error;
5
+ message;
6
+ constructor(message, error) {
7
+ super(message);
8
+ this.message = message;
9
+ this.error = error;
10
+ }
11
+ }
12
+ export class CustomMessageEvent extends MessageEvent {
13
+ }
@@ -0,0 +1,2 @@
1
+ import CodDicomWebServer from './CodDicomWebServer';
2
+ export { CodDicomWebServer };
@@ -0,0 +1,2 @@
1
+ import CodDicomWebServer from './CodDicomWebServer';
2
+ export { CodDicomWebServer };
@@ -0,0 +1,102 @@
1
+ import constants, { Enums } from '../constants';
2
+ import { CustomError } from './customClasses';
3
+ export function parseWadorsURL(url, domain) {
4
+ if (!url.includes(constants.url.URL_VALIDATION_STRING)) {
5
+ return;
6
+ }
7
+ const filePath = url.split(domain + '/')[1];
8
+ const prefix = filePath.split('/studies')[0];
9
+ const prefixParts = prefix.split('/');
10
+ const bucketName = prefixParts[0];
11
+ const bucketPrefix = prefixParts.slice(1).join('/');
12
+ const imagePath = filePath.split(prefix + '/')[1];
13
+ const imageParts = imagePath.split('/');
14
+ const studyInstanceUID = imageParts[1];
15
+ const seriesInstanceUID = imageParts[3];
16
+ let sopInstanceUID = '', frameNumber = 1, type;
17
+ switch (true) {
18
+ case imageParts.includes('thumbnail'):
19
+ type = Enums.RequestType.THUMBNAIL;
20
+ break;
21
+ case imageParts.includes('metadata'):
22
+ if (imageParts.includes('instances')) {
23
+ sopInstanceUID = imageParts[5];
24
+ type = Enums.RequestType.INSTANCE_METADATA;
25
+ }
26
+ else {
27
+ type = Enums.RequestType.SERIES_METADATA;
28
+ }
29
+ break;
30
+ case imageParts.includes('frames'):
31
+ sopInstanceUID = imageParts[5];
32
+ frameNumber = +imageParts[7];
33
+ type = Enums.RequestType.FRAME;
34
+ break;
35
+ default:
36
+ throw new CustomError('Invalid type of request');
37
+ }
38
+ return {
39
+ type,
40
+ bucketName,
41
+ bucketPrefix,
42
+ studyInstanceUID,
43
+ seriesInstanceUID,
44
+ sopInstanceUID,
45
+ frameNumber
46
+ };
47
+ }
48
+ export function getFrameDetailsFromMetadata(seriesMetadata, sopInstanceUID, frameIndex, bucketDetails) {
49
+ if (!seriesMetadata || !seriesMetadata.cod?.instances) {
50
+ throw new CustomError('Invalid seriesMetadata provided.');
51
+ }
52
+ if (frameIndex === null || frameIndex === undefined) {
53
+ throw new CustomError('Frame index is required.');
54
+ }
55
+ const { domain, bucketName, bucketPrefix } = bucketDetails;
56
+ let thumbnailUrl;
57
+ if (seriesMetadata.thumbnail) {
58
+ const thumbnailGsUtilUri = seriesMetadata.thumbnail.uri;
59
+ thumbnailUrl = `${domain}/${thumbnailGsUtilUri.split('gs://')[1]}`;
60
+ }
61
+ const instanceFound = Object.values(seriesMetadata.cod.instances).find((instance) => instance.metadata['00080018']?.Value?.[0] === sopInstanceUID);
62
+ if (!instanceFound) {
63
+ return { thumbnailUrl };
64
+ }
65
+ const { url, uri, headers: offsetHeaders, offset_tables } = instanceFound;
66
+ const modifiedUrl = handleUrl(url || uri, domain, bucketName, bucketPrefix);
67
+ const { CustomOffsetTable, CustomOffsetTableLengths } = offset_tables;
68
+ let sliceStart, sliceEnd, isMultiframe = false;
69
+ if (CustomOffsetTable?.length && CustomOffsetTableLengths?.length) {
70
+ sliceStart = CustomOffsetTable[frameIndex];
71
+ sliceEnd = sliceStart + CustomOffsetTableLengths[frameIndex];
72
+ isMultiframe = true;
73
+ }
74
+ const { start_byte: fileStartByte, end_byte: fileEndByte } = offsetHeaders;
75
+ const startByte = sliceStart !== undefined ? fileStartByte + sliceStart : fileStartByte;
76
+ const endByte = sliceEnd !== undefined ? fileStartByte + sliceEnd : fileEndByte;
77
+ return {
78
+ url: modifiedUrl,
79
+ startByte,
80
+ endByte,
81
+ thumbnailUrl,
82
+ isMultiframe
83
+ };
84
+ }
85
+ export function handleUrl(url, domain, bucketName, bucketPrefix) {
86
+ let modifiedUrl = url;
87
+ const matchingExtension = constants.url.FILE_EXTENSIONS.find((extension) => url.includes(extension));
88
+ if (matchingExtension) {
89
+ const fileParts = url.split(matchingExtension);
90
+ modifiedUrl = fileParts[0] + matchingExtension;
91
+ }
92
+ const filePath = modifiedUrl.split('studies/')[1];
93
+ modifiedUrl = `${domain}/${bucketName}/${bucketPrefix ? bucketPrefix + '/' : ''}studies/${filePath}`;
94
+ return modifiedUrl;
95
+ }
96
+ export function createMetadataJsonUrl(params) {
97
+ const { domain = constants.url.DOMAIN, bucketName, bucketPrefix, studyInstanceUID, seriesInstanceUID } = params;
98
+ if (!bucketName || !bucketPrefix || !studyInstanceUID || !seriesInstanceUID) {
99
+ return;
100
+ }
101
+ return `${domain}/${bucketName}/${bucketPrefix}/studies/${studyInstanceUID}/series/${seriesInstanceUID}/metadata.json`;
102
+ }
@@ -0,0 +1,3 @@
1
+ export const FILE_PARTIAL_WORKER_NAME = 'filePartial';
2
+ export const FILE_STREAMING_WORKER_NAME = 'fileStreaming';
3
+ export const THRESHOLD = 10000;
@@ -15,3 +15,7 @@ export declare enum RequestType {
15
15
  SERIES_METADATA = 2,
16
16
  INSTANCE_METADATA = 3
17
17
  }
18
+ export declare enum DataRetrieveMode {
19
+ WORKER = 0,
20
+ REQUEST = 1
21
+ }
@@ -0,0 +1,24 @@
1
+ export var FetchType;
2
+ (function (FetchType) {
3
+ /**
4
+ * Fetch only the part of the file according to the offsets provided.
5
+ */
6
+ FetchType[FetchType["BYTES_OPTIMIZED"] = 0] = "BYTES_OPTIMIZED";
7
+ /**
8
+ * Stream the file and returns the part of the file if offsets are provided.
9
+ * Or returns the whole file.
10
+ */
11
+ FetchType[FetchType["API_OPTIMIZED"] = 1] = "API_OPTIMIZED";
12
+ })(FetchType || (FetchType = {}));
13
+ export var RequestType;
14
+ (function (RequestType) {
15
+ RequestType[RequestType["FRAME"] = 0] = "FRAME";
16
+ RequestType[RequestType["THUMBNAIL"] = 1] = "THUMBNAIL";
17
+ RequestType[RequestType["SERIES_METADATA"] = 2] = "SERIES_METADATA";
18
+ RequestType[RequestType["INSTANCE_METADATA"] = 3] = "INSTANCE_METADATA";
19
+ })(RequestType || (RequestType = {}));
20
+ export var DataRetrieveMode;
21
+ (function (DataRetrieveMode) {
22
+ DataRetrieveMode[DataRetrieveMode["WORKER"] = 0] = "WORKER";
23
+ DataRetrieveMode[DataRetrieveMode["REQUEST"] = 1] = "REQUEST";
24
+ })(DataRetrieveMode || (DataRetrieveMode = {}));
@@ -1,10 +1,10 @@
1
1
  import * as Enums from './enums';
2
2
  import * as url from './url';
3
- import * as worker from './worker';
3
+ import * as dataRetrieval from './dataRetrieval';
4
4
  declare const constants: {
5
5
  Enums: typeof Enums;
6
6
  url: typeof url;
7
- worker: typeof worker;
7
+ dataRetrieval: typeof dataRetrieval;
8
8
  };
9
- export { Enums, url, worker };
9
+ export { Enums, url, dataRetrieval };
10
10
  export default constants;
@@ -0,0 +1,6 @@
1
+ import * as Enums from './enums';
2
+ import * as url from './url';
3
+ import * as dataRetrieval from './dataRetrieval';
4
+ const constants = { Enums, url, dataRetrieval };
5
+ export { Enums, url, dataRetrieval };
6
+ export default constants;
@@ -0,0 +1,3 @@
1
+ export const DOMAIN = 'https://storage.googleapis.com';
2
+ export const FILE_EXTENSIONS = ['.tar', '.zip'];
3
+ export const URL_VALIDATION_STRING = '/dicomweb/';
@@ -0,0 +1,17 @@
1
+ import { CustomErrorEvent, CustomMessageEvent } from '../classes/customClasses';
2
+ import { Enums } from '../constants';
3
+ import { ScriptObject } from '../types';
4
+ declare class DataRetrievalManager {
5
+ private dataRetriever;
6
+ private dataRetrieverMode;
7
+ constructor();
8
+ getDataRetrieverMode(): Enums.DataRetrieveMode;
9
+ setDataRetrieverMode(mode: Enums.DataRetrieveMode): void;
10
+ register(name: string, arg: (() => Worker) | ScriptObject): void;
11
+ executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<void>;
12
+ addEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
13
+ removeEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
14
+ reset(): void;
15
+ }
16
+ export declare function getDataRetrievalManager(): DataRetrievalManager;
17
+ export {};
@@ -0,0 +1,54 @@
1
+ import { CustomError } from '../classes/customClasses';
2
+ import { Enums } from '../constants';
3
+ import RequestManager from './requestManager';
4
+ import { isNodeEnvironment } from './utils/environment';
5
+ import WebWorkerManager from './workerManager';
6
+ class DataRetrievalManager {
7
+ dataRetriever;
8
+ dataRetrieverMode;
9
+ constructor() {
10
+ if (isNodeEnvironment()) {
11
+ this.dataRetriever = new RequestManager();
12
+ this.dataRetrieverMode = Enums.DataRetrieveMode.REQUEST;
13
+ }
14
+ else {
15
+ this.dataRetriever = new WebWorkerManager();
16
+ this.dataRetrieverMode = Enums.DataRetrieveMode.WORKER;
17
+ }
18
+ }
19
+ getDataRetrieverMode() {
20
+ return this.dataRetrieverMode;
21
+ }
22
+ setDataRetrieverMode(mode) {
23
+ const managers = {
24
+ [Enums.DataRetrieveMode.WORKER]: WebWorkerManager,
25
+ [Enums.DataRetrieveMode.REQUEST]: RequestManager
26
+ };
27
+ if (!(mode in managers)) {
28
+ throw new CustomError('Invalid mode');
29
+ }
30
+ this.dataRetriever.reset();
31
+ this.dataRetriever = new managers[mode]();
32
+ this.dataRetrieverMode = mode;
33
+ }
34
+ register(name, arg) {
35
+ // @ts-ignore
36
+ this.dataRetriever.register(name, arg);
37
+ }
38
+ async executeTask(loaderName, taskName, options) {
39
+ return await this.dataRetriever.executeTask(loaderName, taskName, options);
40
+ }
41
+ addEventListener(workerName, eventType, listener) {
42
+ this.dataRetriever.addEventListener(workerName, eventType, listener);
43
+ }
44
+ removeEventListener(workerName, eventType, listener) {
45
+ this.dataRetriever.removeEventListener(workerName, eventType, listener);
46
+ }
47
+ reset() {
48
+ this.dataRetriever.reset();
49
+ }
50
+ }
51
+ const dataRetrievalManager = new DataRetrievalManager();
52
+ export function getDataRetrievalManager() {
53
+ return dataRetrievalManager;
54
+ }
@@ -0,0 +1,4 @@
1
+ export declare function register(workerNames: {
2
+ fileStreamingScriptName: string;
3
+ filePartialScriptName: string;
4
+ }, maxFetchSize: number): void;
@@ -0,0 +1,25 @@
1
+ import { Enums } from '../constants';
2
+ import { getDataRetrievalManager } from './dataRetrievalManager';
3
+ import filePartial from './scripts/filePartial';
4
+ import fileStreaming from './scripts/fileStreaming';
5
+ export function register(workerNames, maxFetchSize) {
6
+ const { fileStreamingScriptName, filePartialScriptName } = workerNames;
7
+ const dataRetrievalManager = getDataRetrievalManager();
8
+ if (dataRetrievalManager.getDataRetrieverMode() === Enums.DataRetrieveMode.REQUEST) {
9
+ dataRetrievalManager.register(fileStreamingScriptName, fileStreaming);
10
+ dataRetrievalManager.register(filePartialScriptName, filePartial);
11
+ }
12
+ else {
13
+ // fileStreaming worker
14
+ const streamingWorkerFn = () => new Worker(new URL('./workers/fileStreamingWorker', import.meta.url), {
15
+ name: fileStreamingScriptName
16
+ });
17
+ dataRetrievalManager.register(fileStreamingScriptName, streamingWorkerFn);
18
+ // filePartial worker
19
+ const partialWorkerFn = () => new Worker(new URL('./workers/filePartialWorker', import.meta.url), {
20
+ name: filePartialScriptName
21
+ });
22
+ dataRetrievalManager.register(filePartialScriptName, partialWorkerFn);
23
+ }
24
+ dataRetrievalManager.executeTask(fileStreamingScriptName, 'setMaxFetchSize', maxFetchSize);
25
+ }
@@ -0,0 +1,12 @@
1
+ import { CustomMessageEvent, CustomErrorEvent } from '../classes/customClasses';
2
+ import { ScriptObject } from '../types';
3
+ declare class RequestManager {
4
+ private loaderRegistry;
5
+ register(loaderName: string, loaderObject: ScriptObject): void;
6
+ private listenerCallback;
7
+ executeTask(loaderName: string, taskName: string, options: Record<string, unknown> | unknown): Promise<void>;
8
+ addEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
9
+ removeEventListener(workerName: string, eventType: keyof WorkerEventMap, listener: (evt: CustomMessageEvent | CustomErrorEvent) => unknown): void;
10
+ reset(): void;
11
+ }
12
+ export default RequestManager;