@ubercode/dcmtk 0.15.0 → 0.16.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/dicom.cjs +10 -4
- package/dist/dicom.cjs.map +1 -1
- package/dist/dicom.js +10 -4
- package/dist/dicom.js.map +1 -1
- package/dist/{index-D7nnTY1R.d.ts → index-C6dWOhHS.d.ts} +32 -1
- package/dist/{index-CtqMUrjP.d.cts → index-D1msvaAt.d.cts} +32 -1
- package/dist/index.cjs +148 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +148 -11
- package/dist/index.js.map +1 -1
- package/dist/servers.cjs +142 -10
- package/dist/servers.cjs.map +1 -1
- package/dist/servers.d.cts +1 -1
- package/dist/servers.d.ts +1 -1
- package/dist/servers.js +142 -10
- package/dist/servers.js.map +1 -1
- package/dist/tools.cjs +10 -4
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.js +10 -4
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/servers.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AssociationAcknowledgedData, a as AssociationCompleteData, b as AssociationContext, c as AssociationReceivedData, d as AssociationSummary, e as AssociationTracker, C as CStoreRequestData, f as CannotStartListenerData, g as ConfigErrorData, D as DCMPRSCP_FATAL_EVENTS, h as DCMPRSCP_PATTERNS, i as DCMPSRCV_FATAL_EVENTS, j as DCMPSRCV_PATTERNS, k as DCMQRSCP_FATAL_EVENTS, l as DCMQRSCP_PATTERNS, m as DCMRECV_FATAL_EVENTS, n as DCMRECV_PATTERNS, o as DatabaseReadyData, p as DcmQRSCP, q as DcmQRSCPEventMap, r as DcmQRSCPOptions, s as DcmprsCP, t as DcmprsCPEventMap, u as DcmprsCPOptions, v as DcmprscpEvent, w as DcmprscpEventValue, x as Dcmpsrcv, y as DcmpsrcvEvent, z as DcmpsrcvEventMap, B as DcmpsrcvEventValue, E as DcmpsrcvOptions, F as DcmqrscpEvent, G as DcmqrscpEventValue, H as Dcmrecv, I as DcmrecvEvent, J as DcmrecvEventMap, K as DcmrecvEventValue, L as DcmrecvOptions, M as DcmtkProcess, N as DcmtkProcessConfig, O as DcmtkProcessEventMap, P as DicomReceiver, Q as DicomReceiverEventMap, R as DicomReceiverOptions, S as EventPattern, T as FileDeletedData, U as FileReceivedData, V as FilenameMode, W as FilenameModeValue, X as LineParser, Y as LineParserEventMap, Z as MultiLineConfig, a2 as PoolStatus, a3 as PreferredTransferSyntax, a4 as PreferredTransferSyntaxValue, a5 as PrintAssociationAcknowledgedData, a6 as PrintAssociationReceivedData, a7 as PrintCannotStartListenerData, a8 as ProcessState, a9 as ProcessStateValue, aa as QRAssociationAcknowledgedData, ab as QRAssociationReceivedData, ac as QRCFindRequestData, ad as QRCGetRequestData, ae as QRCMoveRequestData, af as QRCStoreRequestData, ag as QRCannotStartListenerData, ah as QRListeningData, ai as ReceiverAssociationAcknowledgedData, aj as ReceiverAssociationData, ak as ReceiverAssociationFinalizedData, al as ReceiverAssociationReceivedData, am as ReceiverCStoreRequestData, an as ReceiverCannotStartListenerData, ao as ReceiverConfigErrorData, ap as ReceiverDatabaseReadyData, aq as ReceiverEchoRequestData, ar as ReceiverErrorData, as as ReceiverFileData, at as ReceiverFileReceivedData, au as ReceiverFileStoredData, av as ReceiverInstanceData, aw as ReceiverInstanceErrorData, ax as ReceiverListeningData, ay as RefusingAssociationData, az as STORESCP_FATAL_EVENTS, aA as STORESCP_PATTERNS, aB as StorageMode, aC as StorageModeValue, aD as StoreSCP, aE as StoreSCPEventMap, aF as StoreSCPOptions, aG as StoreSCPPreset, aH as StoreSCPPresetName, aI as StoredFileData, aJ as StorescpEvent, aK as StorescpEventValue, aL as StoringFileData, aM as SubdirectoryCreatedData, aN as SubdirectoryMode, aO as SubdirectoryModeValue, aP as TrackedFile, aQ as WLMSCPFS_FATAL_EVENTS, aR as WLMSCPFS_PATTERNS, aS as WlmAssociationAcknowledgedData, aT as WlmAssociationReceivedData, aU as WlmCFindRequestData, aV as WlmCannotStartListenerData, aW as WlmListeningData, aX as Wlmscpfs, aY as WlmscpfsEvent, aZ as WlmscpfsEventMap, a_ as WlmscpfsEventValue, a$ as WlmscpfsOptions } from './index-
|
|
1
|
+
export { A as AssociationAcknowledgedData, a as AssociationCompleteData, b as AssociationContext, c as AssociationReceivedData, d as AssociationSummary, e as AssociationTracker, C as CStoreRequestData, f as CannotStartListenerData, g as ConfigErrorData, D as DCMPRSCP_FATAL_EVENTS, h as DCMPRSCP_PATTERNS, i as DCMPSRCV_FATAL_EVENTS, j as DCMPSRCV_PATTERNS, k as DCMQRSCP_FATAL_EVENTS, l as DCMQRSCP_PATTERNS, m as DCMRECV_FATAL_EVENTS, n as DCMRECV_PATTERNS, o as DatabaseReadyData, p as DcmQRSCP, q as DcmQRSCPEventMap, r as DcmQRSCPOptions, s as DcmprsCP, t as DcmprsCPEventMap, u as DcmprsCPOptions, v as DcmprscpEvent, w as DcmprscpEventValue, x as Dcmpsrcv, y as DcmpsrcvEvent, z as DcmpsrcvEventMap, B as DcmpsrcvEventValue, E as DcmpsrcvOptions, F as DcmqrscpEvent, G as DcmqrscpEventValue, H as Dcmrecv, I as DcmrecvEvent, J as DcmrecvEventMap, K as DcmrecvEventValue, L as DcmrecvOptions, M as DcmtkProcess, N as DcmtkProcessConfig, O as DcmtkProcessEventMap, P as DicomReceiver, Q as DicomReceiverEventMap, R as DicomReceiverOptions, S as EventPattern, T as FileDeletedData, U as FileReceivedData, V as FilenameMode, W as FilenameModeValue, X as LineParser, Y as LineParserEventMap, Z as MultiLineConfig, a2 as PoolStatus, a3 as PreferredTransferSyntax, a4 as PreferredTransferSyntaxValue, a5 as PrintAssociationAcknowledgedData, a6 as PrintAssociationReceivedData, a7 as PrintCannotStartListenerData, a8 as ProcessState, a9 as ProcessStateValue, aa as QRAssociationAcknowledgedData, ab as QRAssociationReceivedData, ac as QRCFindRequestData, ad as QRCGetRequestData, ae as QRCMoveRequestData, af as QRCStoreRequestData, ag as QRCannotStartListenerData, ah as QRListeningData, ai as ReceiverAssociationAcknowledgedData, aj as ReceiverAssociationData, ak as ReceiverAssociationFinalizedData, al as ReceiverAssociationReceivedData, am as ReceiverCStoreRequestData, an as ReceiverCannotStartListenerData, ao as ReceiverConfigErrorData, ap as ReceiverDatabaseReadyData, aq as ReceiverEchoRequestData, ar as ReceiverErrorData, as as ReceiverFileData, at as ReceiverFileReceivedData, au as ReceiverFileStoredData, av as ReceiverInstanceData, aw as ReceiverInstanceErrorData, ax as ReceiverListeningData, ay as RefusingAssociationData, az as STORESCP_FATAL_EVENTS, aA as STORESCP_PATTERNS, aB as StorageMode, aC as StorageModeValue, aD as StoreSCP, aE as StoreSCPEventMap, aF as StoreSCPOptions, aG as StoreSCPPreset, aH as StoreSCPPresetName, aI as StoredFileData, aJ as StorescpEvent, aK as StorescpEventValue, aL as StoringFileData, aM as SubdirectoryCreatedData, aN as SubdirectoryMode, aO as SubdirectoryModeValue, aP as TrackedFile, aQ as WLMSCPFS_FATAL_EVENTS, aR as WLMSCPFS_PATTERNS, aS as WlmAssociationAcknowledgedData, aT as WlmAssociationReceivedData, aU as WlmCFindRequestData, aV as WlmCannotStartListenerData, aW as WlmListeningData, aX as Wlmscpfs, aY as WlmscpfsEvent, aZ as WlmscpfsEventMap, a_ as WlmscpfsEventValue, a$ as WlmscpfsOptions } from './index-D1msvaAt.cjs';
|
|
2
2
|
import './types-Cgumy1N4.cjs';
|
|
3
3
|
import 'node:events';
|
|
4
4
|
import 'node:net';
|
package/dist/servers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AssociationAcknowledgedData, a as AssociationCompleteData, b as AssociationContext, c as AssociationReceivedData, d as AssociationSummary, e as AssociationTracker, C as CStoreRequestData, f as CannotStartListenerData, g as ConfigErrorData, D as DCMPRSCP_FATAL_EVENTS, h as DCMPRSCP_PATTERNS, i as DCMPSRCV_FATAL_EVENTS, j as DCMPSRCV_PATTERNS, k as DCMQRSCP_FATAL_EVENTS, l as DCMQRSCP_PATTERNS, m as DCMRECV_FATAL_EVENTS, n as DCMRECV_PATTERNS, o as DatabaseReadyData, p as DcmQRSCP, q as DcmQRSCPEventMap, r as DcmQRSCPOptions, s as DcmprsCP, t as DcmprsCPEventMap, u as DcmprsCPOptions, v as DcmprscpEvent, w as DcmprscpEventValue, x as Dcmpsrcv, y as DcmpsrcvEvent, z as DcmpsrcvEventMap, B as DcmpsrcvEventValue, E as DcmpsrcvOptions, F as DcmqrscpEvent, G as DcmqrscpEventValue, H as Dcmrecv, I as DcmrecvEvent, J as DcmrecvEventMap, K as DcmrecvEventValue, L as DcmrecvOptions, M as DcmtkProcess, N as DcmtkProcessConfig, O as DcmtkProcessEventMap, P as DicomReceiver, Q as DicomReceiverEventMap, R as DicomReceiverOptions, S as EventPattern, T as FileDeletedData, U as FileReceivedData, V as FilenameMode, W as FilenameModeValue, X as LineParser, Y as LineParserEventMap, Z as MultiLineConfig, a2 as PoolStatus, a3 as PreferredTransferSyntax, a4 as PreferredTransferSyntaxValue, a5 as PrintAssociationAcknowledgedData, a6 as PrintAssociationReceivedData, a7 as PrintCannotStartListenerData, a8 as ProcessState, a9 as ProcessStateValue, aa as QRAssociationAcknowledgedData, ab as QRAssociationReceivedData, ac as QRCFindRequestData, ad as QRCGetRequestData, ae as QRCMoveRequestData, af as QRCStoreRequestData, ag as QRCannotStartListenerData, ah as QRListeningData, ai as ReceiverAssociationAcknowledgedData, aj as ReceiverAssociationData, ak as ReceiverAssociationFinalizedData, al as ReceiverAssociationReceivedData, am as ReceiverCStoreRequestData, an as ReceiverCannotStartListenerData, ao as ReceiverConfigErrorData, ap as ReceiverDatabaseReadyData, aq as ReceiverEchoRequestData, ar as ReceiverErrorData, as as ReceiverFileData, at as ReceiverFileReceivedData, au as ReceiverFileStoredData, av as ReceiverInstanceData, aw as ReceiverInstanceErrorData, ax as ReceiverListeningData, ay as RefusingAssociationData, az as STORESCP_FATAL_EVENTS, aA as STORESCP_PATTERNS, aB as StorageMode, aC as StorageModeValue, aD as StoreSCP, aE as StoreSCPEventMap, aF as StoreSCPOptions, aG as StoreSCPPreset, aH as StoreSCPPresetName, aI as StoredFileData, aJ as StorescpEvent, aK as StorescpEventValue, aL as StoringFileData, aM as SubdirectoryCreatedData, aN as SubdirectoryMode, aO as SubdirectoryModeValue, aP as TrackedFile, aQ as WLMSCPFS_FATAL_EVENTS, aR as WLMSCPFS_PATTERNS, aS as WlmAssociationAcknowledgedData, aT as WlmAssociationReceivedData, aU as WlmCFindRequestData, aV as WlmCannotStartListenerData, aW as WlmListeningData, aX as Wlmscpfs, aY as WlmscpfsEvent, aZ as WlmscpfsEventMap, a_ as WlmscpfsEventValue, a$ as WlmscpfsOptions } from './index-
|
|
1
|
+
export { A as AssociationAcknowledgedData, a as AssociationCompleteData, b as AssociationContext, c as AssociationReceivedData, d as AssociationSummary, e as AssociationTracker, C as CStoreRequestData, f as CannotStartListenerData, g as ConfigErrorData, D as DCMPRSCP_FATAL_EVENTS, h as DCMPRSCP_PATTERNS, i as DCMPSRCV_FATAL_EVENTS, j as DCMPSRCV_PATTERNS, k as DCMQRSCP_FATAL_EVENTS, l as DCMQRSCP_PATTERNS, m as DCMRECV_FATAL_EVENTS, n as DCMRECV_PATTERNS, o as DatabaseReadyData, p as DcmQRSCP, q as DcmQRSCPEventMap, r as DcmQRSCPOptions, s as DcmprsCP, t as DcmprsCPEventMap, u as DcmprsCPOptions, v as DcmprscpEvent, w as DcmprscpEventValue, x as Dcmpsrcv, y as DcmpsrcvEvent, z as DcmpsrcvEventMap, B as DcmpsrcvEventValue, E as DcmpsrcvOptions, F as DcmqrscpEvent, G as DcmqrscpEventValue, H as Dcmrecv, I as DcmrecvEvent, J as DcmrecvEventMap, K as DcmrecvEventValue, L as DcmrecvOptions, M as DcmtkProcess, N as DcmtkProcessConfig, O as DcmtkProcessEventMap, P as DicomReceiver, Q as DicomReceiverEventMap, R as DicomReceiverOptions, S as EventPattern, T as FileDeletedData, U as FileReceivedData, V as FilenameMode, W as FilenameModeValue, X as LineParser, Y as LineParserEventMap, Z as MultiLineConfig, a2 as PoolStatus, a3 as PreferredTransferSyntax, a4 as PreferredTransferSyntaxValue, a5 as PrintAssociationAcknowledgedData, a6 as PrintAssociationReceivedData, a7 as PrintCannotStartListenerData, a8 as ProcessState, a9 as ProcessStateValue, aa as QRAssociationAcknowledgedData, ab as QRAssociationReceivedData, ac as QRCFindRequestData, ad as QRCGetRequestData, ae as QRCMoveRequestData, af as QRCStoreRequestData, ag as QRCannotStartListenerData, ah as QRListeningData, ai as ReceiverAssociationAcknowledgedData, aj as ReceiverAssociationData, ak as ReceiverAssociationFinalizedData, al as ReceiverAssociationReceivedData, am as ReceiverCStoreRequestData, an as ReceiverCannotStartListenerData, ao as ReceiverConfigErrorData, ap as ReceiverDatabaseReadyData, aq as ReceiverEchoRequestData, ar as ReceiverErrorData, as as ReceiverFileData, at as ReceiverFileReceivedData, au as ReceiverFileStoredData, av as ReceiverInstanceData, aw as ReceiverInstanceErrorData, ax as ReceiverListeningData, ay as RefusingAssociationData, az as STORESCP_FATAL_EVENTS, aA as STORESCP_PATTERNS, aB as StorageMode, aC as StorageModeValue, aD as StoreSCP, aE as StoreSCPEventMap, aF as StoreSCPOptions, aG as StoreSCPPreset, aH as StoreSCPPresetName, aI as StoredFileData, aJ as StorescpEvent, aK as StorescpEventValue, aL as StoringFileData, aM as SubdirectoryCreatedData, aN as SubdirectoryMode, aO as SubdirectoryModeValue, aP as TrackedFile, aQ as WLMSCPFS_FATAL_EVENTS, aR as WLMSCPFS_PATTERNS, aS as WlmAssociationAcknowledgedData, aT as WlmAssociationReceivedData, aU as WlmCFindRequestData, aV as WlmCannotStartListenerData, aW as WlmListeningData, aX as Wlmscpfs, aY as WlmscpfsEvent, aZ as WlmscpfsEventMap, a_ as WlmscpfsEventValue, a$ as WlmscpfsOptions } from './index-C6dWOhHS.js';
|
|
2
2
|
import './types-Cgumy1N4.js';
|
|
3
3
|
import 'node:events';
|
|
4
4
|
import 'node:net';
|
package/dist/servers.js
CHANGED
|
@@ -2109,8 +2109,14 @@ var KNOWN_VR_CODES = /* @__PURE__ */ new Set([
|
|
|
2109
2109
|
"UT",
|
|
2110
2110
|
"UV"
|
|
2111
2111
|
]);
|
|
2112
|
+
function decodeXmlEntities(value) {
|
|
2113
|
+
return value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
2114
|
+
}
|
|
2115
|
+
function decodeIfString(value) {
|
|
2116
|
+
return typeof value === "string" ? decodeXmlEntities(value) : value;
|
|
2117
|
+
}
|
|
2112
2118
|
function buildPnString(comp) {
|
|
2113
|
-
const parts = [comp.FamilyName ?? "", comp.GivenName ?? "", comp.MiddleName ?? "", comp.NamePrefix ?? "", comp.NameSuffix ?? ""];
|
|
2119
|
+
const parts = [comp.FamilyName ?? "", comp.GivenName ?? "", comp.MiddleName ?? "", comp.NamePrefix ?? "", comp.NameSuffix ?? ""].map(decodeXmlEntities);
|
|
2114
2120
|
let last = parts.length - 1;
|
|
2115
2121
|
for (; last >= 0; last--) {
|
|
2116
2122
|
if (parts[last] !== "") break;
|
|
@@ -2149,9 +2155,9 @@ function convertBulkDataURI(attr, element) {
|
|
|
2149
2155
|
const bulkArray = toArray(attr.BulkDataURI);
|
|
2150
2156
|
const firstBulk = bulkArray[0];
|
|
2151
2157
|
if (typeof firstBulk === "object" && firstBulk !== null && "@_uri" in firstBulk) {
|
|
2152
|
-
element.BulkDataURI = safeString(firstBulk["@_uri"]);
|
|
2158
|
+
element.BulkDataURI = decodeXmlEntities(safeString(firstBulk["@_uri"]));
|
|
2153
2159
|
} else {
|
|
2154
|
-
element.BulkDataURI = safeString(firstBulk);
|
|
2160
|
+
element.BulkDataURI = decodeXmlEntities(safeString(firstBulk));
|
|
2155
2161
|
}
|
|
2156
2162
|
}
|
|
2157
2163
|
function convertPNValue(attr, element) {
|
|
@@ -2195,7 +2201,7 @@ function convertRegularValue(attr, element, vr) {
|
|
|
2195
2201
|
const values = [];
|
|
2196
2202
|
const isNumeric = NUMERIC_JSON_VRS.has(vr);
|
|
2197
2203
|
for (const v of valArray) {
|
|
2198
|
-
const unwrapped = unwrapValue(v);
|
|
2204
|
+
const unwrapped = decodeIfString(unwrapValue(v));
|
|
2199
2205
|
values.push(isNumeric ? coerceNumeric(unwrapped) : unwrapped);
|
|
2200
2206
|
}
|
|
2201
2207
|
if (values.length > 0) element.Value = values;
|
|
@@ -2831,6 +2837,7 @@ var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
|
2831
2837
|
var CONNECTION_RETRY_INTERVAL_MS = 500;
|
|
2832
2838
|
var MAX_CONNECTION_RETRIES = 200;
|
|
2833
2839
|
var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
|
|
2840
|
+
var ABORT_REAP_GRACE_MS = 5e3;
|
|
2834
2841
|
var DicomReceiverOptionsSchema = z.object({
|
|
2835
2842
|
port: z.number().int().min(0).max(65535),
|
|
2836
2843
|
storageDir: z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
|
|
@@ -2868,6 +2875,9 @@ var Worker = class {
|
|
|
2868
2875
|
__publicField(this, "_instancePending", /* @__PURE__ */ new Set());
|
|
2869
2876
|
__publicField(this, "_instancesReceived", 0);
|
|
2870
2877
|
__publicField(this, "_instanceErrors", 0);
|
|
2878
|
+
__publicField(this, "_finalized", false);
|
|
2879
|
+
__publicField(this, "_associationReceivedEmitted", false);
|
|
2880
|
+
__publicField(this, "_reapScheduled", false);
|
|
2871
2881
|
__publicField(this, "_files", []);
|
|
2872
2882
|
__publicField(this, "_fileSizes", []);
|
|
2873
2883
|
__publicField(this, "_outputLines", []);
|
|
@@ -2908,6 +2918,62 @@ var Worker = class {
|
|
|
2908
2918
|
this._instancePending.clear();
|
|
2909
2919
|
this._instancesReceived = 0;
|
|
2910
2920
|
this._instanceErrors = 0;
|
|
2921
|
+
this._finalized = false;
|
|
2922
|
+
this._associationReceivedEmitted = false;
|
|
2923
|
+
this._reapScheduled = false;
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Claims the single abort-reap timer slot for this association. The socket
|
|
2927
|
+
* `cleanup` handler runs on up to four socket events (remote/worker ×
|
|
2928
|
+
* error/close, plus an explicit setup-race call), so without this guard each
|
|
2929
|
+
* teardown would arm its own redundant grace-period timer. Returns true only
|
|
2930
|
+
* for the first caller; subsequent calls return false and arm nothing.
|
|
2931
|
+
*/
|
|
2932
|
+
claimReapSchedule() {
|
|
2933
|
+
if (this._reapScheduled) return false;
|
|
2934
|
+
this._reapScheduled = true;
|
|
2935
|
+
return true;
|
|
2936
|
+
}
|
|
2937
|
+
/** True once per association: the first caller wins the finalize, others get false. */
|
|
2938
|
+
beginFinalizeOnce() {
|
|
2939
|
+
if (this._finalized) return false;
|
|
2940
|
+
this._finalized = true;
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2943
|
+
/** Whether finalize handling has already been claimed for the current association. */
|
|
2944
|
+
get finalized() {
|
|
2945
|
+
return this._finalized;
|
|
2946
|
+
}
|
|
2947
|
+
/** Records that ASSOCIATION_RECEIVED was emitted to consumers for this association. */
|
|
2948
|
+
markAssociationReceived() {
|
|
2949
|
+
this._associationReceivedEmitted = true;
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Whether ASSOCIATION_RECEIVED was emitted for the current association —
|
|
2953
|
+
* i.e. the association was handed off to consumers. When false (a connection
|
|
2954
|
+
* that aborted during/before DICOM negotiation), no consumer ever engaged
|
|
2955
|
+
* with the association directory, so the pool owns its cleanup.
|
|
2956
|
+
*/
|
|
2957
|
+
get associationReceived() {
|
|
2958
|
+
return this._associationReceivedEmitted;
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Atomically reserves a just-selected idle worker so concurrent connection
|
|
2962
|
+
* routing cannot hand the same worker to two associations. Must be called
|
|
2963
|
+
* synchronously the instant an idle worker is picked — before any `await` —
|
|
2964
|
+
* because `handleConnection` yields (e.g. on `ensureDirectory`) between
|
|
2965
|
+
* selecting a worker and calling {@link beginAssociation}. Without this,
|
|
2966
|
+
* two concurrent connections both see the worker as idle, both proceed, and
|
|
2967
|
+
* the second `beginAssociation` overwrites the first's context — orphaning
|
|
2968
|
+
* the first association (no terminal event ever fires for it). Released via
|
|
2969
|
+
* {@link release} if association setup fails before `beginAssociation`.
|
|
2970
|
+
*/
|
|
2971
|
+
reserve() {
|
|
2972
|
+
this._state = "busy";
|
|
2973
|
+
}
|
|
2974
|
+
/** Returns a reserved-but-not-yet-begun worker to the idle pool (setup failed). */
|
|
2975
|
+
release() {
|
|
2976
|
+
this._state = "idle";
|
|
2911
2977
|
}
|
|
2912
2978
|
/** Marks the worker as finalizing — still has valid context but cannot accept new connections. */
|
|
2913
2979
|
markFinalizing() {
|
|
@@ -3293,6 +3359,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3293
3359
|
const associationDir = path.join(this.options.storageDir, associationId);
|
|
3294
3360
|
const mkdirResult = await ensureDirectory(associationDir);
|
|
3295
3361
|
if (!mkdirResult.ok) {
|
|
3362
|
+
worker.release();
|
|
3296
3363
|
remoteSocket.destroy();
|
|
3297
3364
|
this.emit("error", { error: mkdirResult.error });
|
|
3298
3365
|
return;
|
|
@@ -3311,12 +3378,18 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3311
3378
|
/** Finds an idle worker, retrying up to connectionTimeoutMs. */
|
|
3312
3379
|
async findIdleWorker() {
|
|
3313
3380
|
const idle = this.getIdleWorker();
|
|
3314
|
-
if (idle !== void 0)
|
|
3381
|
+
if (idle !== void 0) {
|
|
3382
|
+
idle.reserve();
|
|
3383
|
+
return idle;
|
|
3384
|
+
}
|
|
3315
3385
|
const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
|
|
3316
3386
|
for (let i = 0; i < maxRetries; i++) {
|
|
3317
3387
|
await setTimeout$1(CONNECTION_RETRY_INTERVAL_MS);
|
|
3318
3388
|
const found = this.getIdleWorker();
|
|
3319
|
-
if (found !== void 0)
|
|
3389
|
+
if (found !== void 0) {
|
|
3390
|
+
found.reserve();
|
|
3391
|
+
return found;
|
|
3392
|
+
}
|
|
3320
3393
|
if (this.stopping) return void 0;
|
|
3321
3394
|
}
|
|
3322
3395
|
return void 0;
|
|
@@ -3332,11 +3405,13 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3332
3405
|
pipeConnection(worker, remoteSocket) {
|
|
3333
3406
|
const workerSocket = net.createConnection({ port: worker.port, host: "127.0.0.1" });
|
|
3334
3407
|
worker.setSockets(remoteSocket, workerSocket);
|
|
3408
|
+
const ctx = worker.context;
|
|
3335
3409
|
const cleanup = () => {
|
|
3336
3410
|
remoteSocket.unpipe(workerSocket);
|
|
3337
3411
|
workerSocket.unpipe(remoteSocket);
|
|
3338
3412
|
if (!remoteSocket.destroyed) remoteSocket.destroy();
|
|
3339
3413
|
if (!workerSocket.destroyed) workerSocket.destroy();
|
|
3414
|
+
this.scheduleAbortReap(worker, ctx);
|
|
3340
3415
|
};
|
|
3341
3416
|
workerSocket.on("connect", () => {
|
|
3342
3417
|
remoteSocket.pipe(workerSocket);
|
|
@@ -3347,6 +3422,42 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3347
3422
|
workerSocket.on("error", cleanup);
|
|
3348
3423
|
remoteSocket.on("close", cleanup);
|
|
3349
3424
|
workerSocket.on("close", cleanup);
|
|
3425
|
+
if (remoteSocket.destroyed) {
|
|
3426
|
+
cleanup();
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
/**
|
|
3430
|
+
* After a connection's sockets tear down, reaps the association as an
|
|
3431
|
+
* aborted one *only if* dcmrecv never reported completion within
|
|
3432
|
+
* {@link ABORT_REAP_GRACE_MS}. Without this, a peer abort destroys the
|
|
3433
|
+
* sockets but dcmrecv may never emit ASSOCIATION_COMPLETE, so the worker
|
|
3434
|
+
* stays busy forever, ASSOCIATION_FINALIZED never fires (consumers can't
|
|
3435
|
+
* release per-association state), and the association directory is never
|
|
3436
|
+
* removed. The grace delay ensures a normal A-RELEASE — whose socket also
|
|
3437
|
+
* closes — is finalized by its own completion report and never reaped here.
|
|
3438
|
+
*/
|
|
3439
|
+
scheduleAbortReap(worker, ctx) {
|
|
3440
|
+
if (ctx === void 0 || worker.context !== ctx || worker.state !== "busy") return;
|
|
3441
|
+
if (!worker.claimReapSchedule()) return;
|
|
3442
|
+
const timer = setTimeout(() => {
|
|
3443
|
+
if (worker.context !== ctx || worker.state !== "busy" || worker.finalized) return;
|
|
3444
|
+
worker.markFinalizing();
|
|
3445
|
+
const removeDir = !worker.associationReceived;
|
|
3446
|
+
void this.finalizeAssociation(
|
|
3447
|
+
worker,
|
|
3448
|
+
{
|
|
3449
|
+
associationId: ctx.associationId,
|
|
3450
|
+
callingAE: "",
|
|
3451
|
+
calledAE: "",
|
|
3452
|
+
source: "",
|
|
3453
|
+
files: [],
|
|
3454
|
+
durationMs: Date.now() - ctx.startAt,
|
|
3455
|
+
endReason: "abort"
|
|
3456
|
+
},
|
|
3457
|
+
removeDir
|
|
3458
|
+
);
|
|
3459
|
+
}, ABORT_REAP_GRACE_MS);
|
|
3460
|
+
timer.unref();
|
|
3350
3461
|
}
|
|
3351
3462
|
// -----------------------------------------------------------------------
|
|
3352
3463
|
// Worker pool management
|
|
@@ -3545,19 +3656,38 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3545
3656
|
/** Returns worker to idle pool on association complete, emits summary. */
|
|
3546
3657
|
wireAssociationComplete(worker) {
|
|
3547
3658
|
worker.dcmrecv.onAssociationComplete((data) => {
|
|
3659
|
+
if (worker.finalized) return;
|
|
3548
3660
|
worker.markFinalizing();
|
|
3661
|
+
const removeDir = !worker.associationReceived;
|
|
3549
3662
|
setImmediate(() => {
|
|
3550
|
-
void this.finalizeAssociation(worker, data);
|
|
3663
|
+
void this.finalizeAssociation(worker, data, removeDir);
|
|
3551
3664
|
});
|
|
3552
3665
|
});
|
|
3553
3666
|
}
|
|
3554
|
-
/**
|
|
3555
|
-
|
|
3667
|
+
/**
|
|
3668
|
+
* Awaits file ops, emits ASSOCIATION_COMPLETE, awaits parsing, emits
|
|
3669
|
+
* ASSOCIATION_FINALIZED, returns the worker to idle. Runs at most once per
|
|
3670
|
+
* association via {@link Worker.beginFinalizeOnce}.
|
|
3671
|
+
*
|
|
3672
|
+
* @param removeDir - When true, removes the association directory after
|
|
3673
|
+
* finalizing. Set when no consumer ever engaged with the association
|
|
3674
|
+
* (no ASSOCIATION_RECEIVED was emitted) — e.g. a connection that aborted
|
|
3675
|
+
* during negotiation — so the pool reclaims the directory it created.
|
|
3676
|
+
* When a consumer did engage this stays false: the consumer owns
|
|
3677
|
+
* directory cleanup once it has processed the received files (removing it
|
|
3678
|
+
* here could race in-flight consumer reads of those files).
|
|
3679
|
+
*/
|
|
3680
|
+
async finalizeAssociation(worker, data, removeDir = false) {
|
|
3681
|
+
if (!worker.beginFinalizeOnce()) return;
|
|
3556
3682
|
await worker.drainPendingFiles();
|
|
3557
3683
|
this.emitAssociationComplete(worker, data);
|
|
3558
3684
|
await worker.drainInstancePending();
|
|
3559
3685
|
this.emitAssociationFinalized(worker, data);
|
|
3686
|
+
const associationDir = worker.context?.associationDir;
|
|
3560
3687
|
worker.endAssociation();
|
|
3688
|
+
if (removeDir && associationDir !== void 0) {
|
|
3689
|
+
await removeDirSafe(associationDir);
|
|
3690
|
+
}
|
|
3561
3691
|
void this.scaleDown();
|
|
3562
3692
|
}
|
|
3563
3693
|
/** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
|
|
@@ -3599,12 +3729,14 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3599
3729
|
source: data.source,
|
|
3600
3730
|
files: [...worker.files],
|
|
3601
3731
|
instancesReceived: worker.instancesReceived,
|
|
3602
|
-
instanceErrors: worker.instanceErrors
|
|
3732
|
+
instanceErrors: worker.instanceErrors,
|
|
3733
|
+
endReason: data.endReason
|
|
3603
3734
|
});
|
|
3604
3735
|
}
|
|
3605
3736
|
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
3606
3737
|
wireAssociationReceived(worker) {
|
|
3607
3738
|
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
3739
|
+
worker.markAssociationReceived();
|
|
3608
3740
|
this.emit("ASSOCIATION_RECEIVED", {
|
|
3609
3741
|
associationId: worker.context?.associationId ?? "",
|
|
3610
3742
|
callingAE: data.callingAE,
|