cod-dicomweb-server 1.0.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.
- package/dist/assets/js/430.ae979bb9f7321087b4cd.js +2 -0
- package/dist/assets/js/430.ae979bb9f7321087b4cd.js.LICENSE.txt +5 -0
- package/dist/assets/js/663.f8ac8210581651c53c7e.js +2 -0
- package/dist/assets/js/663.f8ac8210581651c53c7e.js.LICENSE.txt +5 -0
- package/dist/assets/js/main.bd27b3d8a119b2e0661f.js +2 -0
- package/dist/assets/js/main.bd27b3d8a119b2e0661f.js.LICENSE.txt +7 -0
- package/dist/classes/CodDicomWebServer.ts +423 -0
- package/dist/classes/utils.ts +176 -0
- package/dist/constants/enums.ts +18 -0
- package/dist/constants/index.ts +8 -0
- package/dist/constants/url.ts +5 -0
- package/dist/constants/worker.ts +4 -0
- package/dist/fileManager.ts +84 -0
- package/dist/index.html +1 -0
- package/dist/index.ts +5 -0
- package/dist/metadataManager.ts +32 -0
- package/dist/types/codDicomWebServerOptions.ts +7 -0
- package/dist/types/fileManagerOptions.ts +3 -0
- package/dist/types/index.ts +7 -0
- package/dist/types/metadata.ts +57 -0
- package/dist/types/metadataUrlCreationParams.ts +9 -0
- package/dist/types/parsedWadoRsUrlDetails.ts +13 -0
- package/dist/types/requestOptions.ts +12 -0
- package/dist/types/workerCustomMessageEvents.ts +11 -0
- package/dist/webWorker/registerWorker.ts +33 -0
- package/dist/webWorker/workerManager.ts +83 -0
- package/dist/webWorker/workers/filePartial.ts +20 -0
- package/dist/webWorker/workers/fileStreaming.ts +130 -0
- package/package.json +86 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import constants, { Enums } from '../constants';
|
|
2
|
+
import type {
|
|
3
|
+
JsonMetadata,
|
|
4
|
+
MetadataUrlCreationParams,
|
|
5
|
+
ParsedWadoRsUrlDetails,
|
|
6
|
+
} from '../types';
|
|
7
|
+
|
|
8
|
+
export function parseWadorsURL(
|
|
9
|
+
url: string,
|
|
10
|
+
domain: string,
|
|
11
|
+
): ParsedWadoRsUrlDetails | undefined {
|
|
12
|
+
if (!url.includes(constants.url.URL_VALIDATION_STRING)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const filePath = url.split(domain + '/')[1];
|
|
17
|
+
|
|
18
|
+
const prefix = filePath.split('/studies')[0];
|
|
19
|
+
const prefixParts = prefix.split('/');
|
|
20
|
+
|
|
21
|
+
const bucketName = prefixParts[0];
|
|
22
|
+
const bucketPrefix = prefixParts.slice(1).join('/');
|
|
23
|
+
|
|
24
|
+
const imagePath = filePath.split(prefix + '/')[1];
|
|
25
|
+
const imageParts = imagePath.split('/');
|
|
26
|
+
|
|
27
|
+
const studyInstanceUID = imageParts[1];
|
|
28
|
+
const seriesInstanceUID = imageParts[3];
|
|
29
|
+
let sopInstanceUID = '',
|
|
30
|
+
frameNumber = 1,
|
|
31
|
+
type: Enums.RequestType;
|
|
32
|
+
|
|
33
|
+
switch (true) {
|
|
34
|
+
case imageParts.includes('thumbnail'):
|
|
35
|
+
type = Enums.RequestType.THUMBNAIL;
|
|
36
|
+
break;
|
|
37
|
+
case imageParts.includes('metadata'):
|
|
38
|
+
if (imageParts.includes('instances')) {
|
|
39
|
+
sopInstanceUID = imageParts[5];
|
|
40
|
+
type = Enums.RequestType.INSTANCE_METADATA;
|
|
41
|
+
} else {
|
|
42
|
+
type = Enums.RequestType.SERIES_METADATA;
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
case imageParts.includes('frames'):
|
|
46
|
+
sopInstanceUID = imageParts[5];
|
|
47
|
+
frameNumber = +(imageParts[7] || frameNumber);
|
|
48
|
+
type = Enums.RequestType.FRAME;
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
throw new Error('Invalid type of request');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
type,
|
|
56
|
+
bucketName,
|
|
57
|
+
bucketPrefix,
|
|
58
|
+
studyInstanceUID,
|
|
59
|
+
seriesInstanceUID,
|
|
60
|
+
sopInstanceUID,
|
|
61
|
+
frameNumber,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getFrameDetailsFromMetadata(
|
|
66
|
+
seriesMetadata: JsonMetadata,
|
|
67
|
+
sopInstanceUID: string,
|
|
68
|
+
frameIndex: number,
|
|
69
|
+
bucketDetails: { domain: string; bucketName: string; bucketPrefix: string },
|
|
70
|
+
): {
|
|
71
|
+
url: string;
|
|
72
|
+
startByte: number;
|
|
73
|
+
endByte: number;
|
|
74
|
+
thumbnailUrl: string;
|
|
75
|
+
isMultiframe: boolean;
|
|
76
|
+
} {
|
|
77
|
+
if (
|
|
78
|
+
!seriesMetadata ||
|
|
79
|
+
!seriesMetadata.cod?.instances ||
|
|
80
|
+
!seriesMetadata.thumbnail
|
|
81
|
+
) {
|
|
82
|
+
throw new Error('Invalid seriesMetadata provided.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!sopInstanceUID) {
|
|
86
|
+
throw new Error('SOP Instance UID is required.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (frameIndex === null || frameIndex === undefined) {
|
|
90
|
+
throw new Error('Frame index is required.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const instanceFound = Object.values(seriesMetadata.cod.instances).find(
|
|
94
|
+
(instance) => instance.metadata['00080018']?.Value?.[0] === sopInstanceUID,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!instanceFound) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Instance with SOPInstanceUID ${sopInstanceUID} not found.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { domain, bucketName, bucketPrefix } = bucketDetails;
|
|
104
|
+
const { url, uri, headers: offsetHeaders, offset_tables } = instanceFound;
|
|
105
|
+
const modifiedUrl = handleUrl(url || uri, domain, bucketName, bucketPrefix);
|
|
106
|
+
|
|
107
|
+
const { CustomOffsetTable, CustomOffsetTableLengths } = offset_tables || {};
|
|
108
|
+
|
|
109
|
+
let sliceStart: number | undefined,
|
|
110
|
+
sliceEnd: number | undefined,
|
|
111
|
+
isMultiframe = false;
|
|
112
|
+
if (CustomOffsetTable?.length && CustomOffsetTableLengths?.length) {
|
|
113
|
+
sliceStart = CustomOffsetTable[frameIndex];
|
|
114
|
+
sliceEnd = sliceStart + CustomOffsetTableLengths[frameIndex];
|
|
115
|
+
isMultiframe = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { start_byte: fileStartByte, end_byte: fileEndByte } = offsetHeaders;
|
|
119
|
+
|
|
120
|
+
const startByte =
|
|
121
|
+
sliceStart !== undefined ? fileStartByte + sliceStart : fileStartByte;
|
|
122
|
+
const endByte =
|
|
123
|
+
sliceEnd !== undefined ? fileStartByte + sliceEnd : fileEndByte;
|
|
124
|
+
|
|
125
|
+
const thumbnailGsUtilUri = seriesMetadata.thumbnail.uri;
|
|
126
|
+
const thumbnailUrl = `${domain}/${thumbnailGsUtilUri.split('gs://')[1]}`;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
url: modifiedUrl,
|
|
130
|
+
startByte,
|
|
131
|
+
endByte,
|
|
132
|
+
thumbnailUrl,
|
|
133
|
+
isMultiframe,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function handleUrl(
|
|
138
|
+
url: string,
|
|
139
|
+
domain: string,
|
|
140
|
+
bucketName: string,
|
|
141
|
+
bucketPrefix: string,
|
|
142
|
+
): string {
|
|
143
|
+
let modifiedUrl = url;
|
|
144
|
+
|
|
145
|
+
const matchingExtension = constants.url.FILE_EXTENSIONS.find((extension) =>
|
|
146
|
+
url.includes(extension),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (matchingExtension) {
|
|
150
|
+
const fileParts = url.split(matchingExtension);
|
|
151
|
+
modifiedUrl = fileParts[0] + matchingExtension;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const filePath = modifiedUrl.split('studies/')[1];
|
|
155
|
+
modifiedUrl = `${domain}/${bucketName}/${bucketPrefix ? bucketPrefix + '/' : ''}studies/${filePath}`;
|
|
156
|
+
|
|
157
|
+
return modifiedUrl;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createMetadataJsonUrl(
|
|
161
|
+
params: MetadataUrlCreationParams,
|
|
162
|
+
): string | undefined {
|
|
163
|
+
const {
|
|
164
|
+
domain = constants.url.DOMAIN,
|
|
165
|
+
bucketName,
|
|
166
|
+
bucketPrefix,
|
|
167
|
+
studyInstanceUID,
|
|
168
|
+
seriesInstanceUID,
|
|
169
|
+
} = params;
|
|
170
|
+
|
|
171
|
+
if (!bucketName || !bucketPrefix || !studyInstanceUID || !seriesInstanceUID) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return `${domain}/${bucketName}/${bucketPrefix}/studies/${studyInstanceUID}/series/${seriesInstanceUID}/metadata.json`;
|
|
176
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export enum FetchType {
|
|
2
|
+
/**
|
|
3
|
+
* Fetch only the part of the file according to the offsets provided.
|
|
4
|
+
*/
|
|
5
|
+
BYTES_OPTIMIZED,
|
|
6
|
+
/**
|
|
7
|
+
* Stream the file and returns the part of the file if offsets are provided.
|
|
8
|
+
* Or returns the whole file.
|
|
9
|
+
*/
|
|
10
|
+
API_OPTIMIZED,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export enum RequestType {
|
|
14
|
+
FRAME,
|
|
15
|
+
THUMBNAIL,
|
|
16
|
+
SERIES_METADATA,
|
|
17
|
+
INSTANCE_METADATA,
|
|
18
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { FileManagerOptions } from './types';
|
|
2
|
+
import { getWebWorkerManager } from './webWorker/workerManager';
|
|
3
|
+
|
|
4
|
+
class FileManager {
|
|
5
|
+
private files: Record<string, { data: Uint8Array; position: number }> = {};
|
|
6
|
+
private fileStreamingWorkerName: string;
|
|
7
|
+
|
|
8
|
+
constructor({ fileStreamingWorkerName }: FileManagerOptions) {
|
|
9
|
+
this.fileStreamingWorkerName = fileStreamingWorkerName;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
set(url: string, file: { data: Uint8Array; position: number }): void {
|
|
13
|
+
this.files[url] = file;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(
|
|
17
|
+
url: string,
|
|
18
|
+
offsets?: { startByte: number; endByte: number },
|
|
19
|
+
): Uint8Array | null {
|
|
20
|
+
if (
|
|
21
|
+
!this.files[url] ||
|
|
22
|
+
(offsets && this.files[url].position <= offsets.endByte)
|
|
23
|
+
) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return offsets
|
|
28
|
+
? this.files[url].data.slice(offsets.startByte, offsets.endByte)
|
|
29
|
+
: this.files[url].data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setPosition(url: string, position: number): void {
|
|
33
|
+
if (this.files[url]) {
|
|
34
|
+
this.files[url].position = position;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getPosition(url: string): number {
|
|
39
|
+
return this.files[url]?.position;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
append(url: string, chunk: Uint8Array, position: number): void {
|
|
43
|
+
if (this.files[url] && position) {
|
|
44
|
+
this.files[url].data.set(chunk, position - chunk.length);
|
|
45
|
+
this.setPosition(url, position);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getTotalSize(): number {
|
|
50
|
+
return Object.entries(this.files).reduce((total, [url, { position }]) => {
|
|
51
|
+
return url.includes('?bytes=') ? total : total + position;
|
|
52
|
+
}, 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
remove(url: string): void {
|
|
56
|
+
const removedSize = this.getPosition(url);
|
|
57
|
+
delete this.files[url];
|
|
58
|
+
|
|
59
|
+
if (url.includes('?bytes=')) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const workerManager = getWebWorkerManager();
|
|
64
|
+
workerManager.executeTask(
|
|
65
|
+
this.fileStreamingWorkerName,
|
|
66
|
+
'decreaseFetchedSize',
|
|
67
|
+
removedSize,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
purge(): void {
|
|
72
|
+
const totalSize = this.getTotalSize();
|
|
73
|
+
this.files = {};
|
|
74
|
+
|
|
75
|
+
const workerManager = getWebWorkerManager();
|
|
76
|
+
workerManager.executeTask(
|
|
77
|
+
this.fileStreamingWorkerName,
|
|
78
|
+
'decreaseFetchedSize',
|
|
79
|
+
totalSize,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export default FileManager;
|
package/dist/index.html
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta http-equiv="X-UA-Compatible" content="ie=edge"/><link rel="icon" href="/favicon.ico" type="image/x-icon"/><link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/><title></title></head><body><noscript><strong>We're sorry but doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="root" class="root"></div><script defer="defer" src="/assets/js/main.bd27b3d8a119b2e0661f.js"></script></body></html>
|
package/dist/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createMetadataJsonUrl } from './classes/utils';
|
|
2
|
+
import type { JsonMetadata, MetadataUrlCreationParams } from './types';
|
|
3
|
+
|
|
4
|
+
const metadata: Record<string, JsonMetadata> = {};
|
|
5
|
+
|
|
6
|
+
export async function getMetadata(
|
|
7
|
+
params: MetadataUrlCreationParams,
|
|
8
|
+
headers: Record<string, string>,
|
|
9
|
+
): Promise<JsonMetadata | null> {
|
|
10
|
+
const url = createMetadataJsonUrl(params);
|
|
11
|
+
|
|
12
|
+
if (!url) {
|
|
13
|
+
throw new Error('Error creating metadata json url');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (metadata[url]) {
|
|
17
|
+
return metadata[url];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(url, { headers });
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Failed to fetch metadata: ${response.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
metadata[url] = data;
|
|
27
|
+
return data;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type * from './codDicomWebServerOptions';
|
|
2
|
+
export type * from './fileManagerOptions';
|
|
3
|
+
export type * from './metadata';
|
|
4
|
+
export type * from './metadataUrlCreationParams';
|
|
5
|
+
export type * from './parsedWadoRsUrlDetails';
|
|
6
|
+
export type * from './requestOptions';
|
|
7
|
+
export type * from './workerCustomMessageEvents';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata format stored in the metadata.json
|
|
3
|
+
*/
|
|
4
|
+
type JsonMetadata = {
|
|
5
|
+
deid_study_uid: string;
|
|
6
|
+
deid_series_uid: string;
|
|
7
|
+
cod: {
|
|
8
|
+
instances: Record<
|
|
9
|
+
string,
|
|
10
|
+
{
|
|
11
|
+
metadata: InstanceMetadata;
|
|
12
|
+
// The metadata will either have url or uri
|
|
13
|
+
uri: string;
|
|
14
|
+
url: string;
|
|
15
|
+
headers: { start_byte: number; end_byte: number };
|
|
16
|
+
offset_tables: {
|
|
17
|
+
CustomOffsetTable?: number[];
|
|
18
|
+
CustomOffsetTableLengths?: number[];
|
|
19
|
+
};
|
|
20
|
+
crc32c: string;
|
|
21
|
+
size: number;
|
|
22
|
+
original_path: string;
|
|
23
|
+
dependencies: string[];
|
|
24
|
+
diff_hash_dupe_paths: [string];
|
|
25
|
+
version: string;
|
|
26
|
+
modified_datetime: string;
|
|
27
|
+
}
|
|
28
|
+
>;
|
|
29
|
+
};
|
|
30
|
+
thumbnail: {
|
|
31
|
+
version: string;
|
|
32
|
+
uri: string;
|
|
33
|
+
thumbnail_index_to_instance_frame: [string, number][];
|
|
34
|
+
instances: Record<
|
|
35
|
+
string,
|
|
36
|
+
{
|
|
37
|
+
frames: {
|
|
38
|
+
thumbnail_index: number;
|
|
39
|
+
anchors: {
|
|
40
|
+
original_size: { width: number; height: number };
|
|
41
|
+
thumbnail_upper_left: { row: number; col: number };
|
|
42
|
+
thumbnail_bottom_right: { row: number; col: number };
|
|
43
|
+
};
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type InstanceMetadata = Record<
|
|
51
|
+
string,
|
|
52
|
+
{ vr: string; Value?: unknown[]; BulkDataURI?: string; InlineBinary?: string }
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
type SeriesMetadata = InstanceMetadata[];
|
|
56
|
+
|
|
57
|
+
export type { InstanceMetadata, SeriesMetadata, JsonMetadata };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Enums } from '../constants';
|
|
2
|
+
|
|
3
|
+
type ParsedWadoRsUrlDetails = {
|
|
4
|
+
type: Enums.RequestType;
|
|
5
|
+
bucketName: string;
|
|
6
|
+
bucketPrefix: string;
|
|
7
|
+
studyInstanceUID: string;
|
|
8
|
+
seriesInstanceUID: string;
|
|
9
|
+
sopInstanceUID: string;
|
|
10
|
+
frameNumber: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type { ParsedWadoRsUrlDetails };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Enums } from '../constants';
|
|
2
|
+
|
|
3
|
+
type CODRequestOptions = {
|
|
4
|
+
useSharedArrayBuffer?: boolean;
|
|
5
|
+
fetchType?: Enums.FetchType;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type FileRequestOptions = CODRequestOptions & {
|
|
9
|
+
offsets?: { startByte: number; endByte: number };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type { CODRequestOptions, FileRequestOptions };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getWebWorkerManager } from './workerManager';
|
|
2
|
+
|
|
3
|
+
export function registerWorkers(
|
|
4
|
+
workerNames: {
|
|
5
|
+
fileStreamingWorkerName: string;
|
|
6
|
+
filePartialWorkerName: string;
|
|
7
|
+
},
|
|
8
|
+
maxFetchSize: number,
|
|
9
|
+
): void {
|
|
10
|
+
const { fileStreamingWorkerName, filePartialWorkerName } = workerNames;
|
|
11
|
+
const workerManager = getWebWorkerManager();
|
|
12
|
+
|
|
13
|
+
// fileStreaming worker
|
|
14
|
+
const streamingWorkerFn = (): Worker =>
|
|
15
|
+
new Worker(new URL('./workers/fileStreaming.ts', import.meta.url), {
|
|
16
|
+
name: fileStreamingWorkerName,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
workerManager.registerWorker(fileStreamingWorkerName, streamingWorkerFn);
|
|
20
|
+
workerManager.executeTask(
|
|
21
|
+
fileStreamingWorkerName,
|
|
22
|
+
'setMaxFetchSize',
|
|
23
|
+
maxFetchSize,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// filePartial worker
|
|
27
|
+
const partialWorkerFn = (): Worker =>
|
|
28
|
+
new Worker(new URL('./workers/filePartial.ts', import.meta.url), {
|
|
29
|
+
name: filePartialWorkerName,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
workerManager.registerWorker(filePartialWorkerName, partialWorkerFn);
|
|
33
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type Remote, wrap } from 'comlink';
|
|
2
|
+
|
|
3
|
+
import type { FileStreamingMessageEvent } from '../types';
|
|
4
|
+
|
|
5
|
+
class WebWorkerManager {
|
|
6
|
+
private workerRegistry: Record<
|
|
7
|
+
string,
|
|
8
|
+
{ instance: Remote<Worker>; nativeWorker: Worker }
|
|
9
|
+
> = {};
|
|
10
|
+
|
|
11
|
+
public registerWorker(name: string, workerFn: () => Worker): void {
|
|
12
|
+
try {
|
|
13
|
+
const worker: Worker = workerFn();
|
|
14
|
+
if (!worker) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.workerRegistry[name] = {
|
|
19
|
+
instance: wrap(worker),
|
|
20
|
+
nativeWorker: worker,
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.warn(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async executeTask(
|
|
28
|
+
workerName: string,
|
|
29
|
+
taskName: string,
|
|
30
|
+
options: Record<string, unknown> | unknown,
|
|
31
|
+
): Promise<void | ArrayBufferLike> {
|
|
32
|
+
const worker = this.workerRegistry[workerName]?.instance;
|
|
33
|
+
if (!worker) {
|
|
34
|
+
throw new Error(`Worker ${workerName} not registered`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
return await worker[taskName](options);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(
|
|
43
|
+
`Error executing task "${taskName}" on worker "${workerName}":`,
|
|
44
|
+
error,
|
|
45
|
+
);
|
|
46
|
+
throw new Error(`Task "${taskName}" failed: ${(error as Error).message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public addEventListener(
|
|
51
|
+
workerName: string,
|
|
52
|
+
eventType: keyof WorkerEventMap,
|
|
53
|
+
listener: (evt: FileStreamingMessageEvent | ErrorEvent) => unknown,
|
|
54
|
+
): void {
|
|
55
|
+
const worker = this.workerRegistry[workerName];
|
|
56
|
+
if (!worker) {
|
|
57
|
+
console.error(`Worker type '${workerName}' is not registered.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
worker.nativeWorker.addEventListener(eventType, listener);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public removeEventListener(
|
|
65
|
+
workerName: string,
|
|
66
|
+
eventType: keyof WorkerEventMap,
|
|
67
|
+
listener: (evt: FileStreamingMessageEvent | ErrorEvent) => unknown,
|
|
68
|
+
): void {
|
|
69
|
+
const worker = this.workerRegistry[workerName];
|
|
70
|
+
if (!worker) {
|
|
71
|
+
console.error(`Worker type '${workerName}' is not registered.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
worker.nativeWorker.removeEventListener(eventType, listener);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const webWorkerManager = new WebWorkerManager();
|
|
80
|
+
|
|
81
|
+
export function getWebWorkerManager(): WebWorkerManager {
|
|
82
|
+
return webWorkerManager;
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { expose } from 'comlink';
|
|
2
|
+
|
|
3
|
+
const filePartial = {
|
|
4
|
+
async partial(args: {
|
|
5
|
+
url: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}): Promise<ArrayBufferLike | Error> {
|
|
8
|
+
const { url, headers } = args;
|
|
9
|
+
|
|
10
|
+
return fetch(url, { headers })
|
|
11
|
+
.then((response) => response.arrayBuffer())
|
|
12
|
+
.catch((error) => {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'filePartial.ts: Error when fetching file: ' + error?.message,
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
expose(filePartial);
|