@ubercode/dcmtk 0.15.1 → 0.16.1

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/index.d.cts CHANGED
@@ -3,7 +3,7 @@ export { L as LineSource, P as ProcessLine, a as ResultValue, b as assertUnreach
3
3
  import { A as AETitle, D as DicomTag, a as DicomTagPath, P as Port, S as SOPClassUID, T as TransferSyntaxUID, b as DicomDataset } from './DicomInstance-BGXtON9T.cjs';
4
4
  export { B as Brand, C as ChangeSet, c as DicomFilePath, d as DicomInstance, e as DicomOpenOptions, W as WalkTagEntry, f as WalkTagsOptions, g as createAETitle, h as createDicomFilePath, i as createDicomTag, j as createDicomTagPath, k as createPort, l as createSOPClassUID, m as createTransferSyntaxUID, t as tag, w as walkTags } from './DicomInstance-BGXtON9T.cjs';
5
5
  import { z } from 'zod';
6
- 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, _ as PoolAssociationReceivedData, $ as PoolCStoreRequestData, a0 as PoolEchoRequestData, a1 as PoolRefusingAssociationData, 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-CtqMUrjP.cjs';
6
+ 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, _ as PoolAssociationReceivedData, $ as PoolCStoreRequestData, a0 as PoolEchoRequestData, a1 as PoolRefusingAssociationData, 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';
7
7
  import { T as TagModification } from './dcmodify-Cf-RPHF3.cjs';
8
8
  export { b as DcmodifyOptions, c as DcmodifyResult, D as DicomJsonElement, a as DicomJsonModel, d as ToolBaseOptions, e as dcmodify, x as xmlToJson } from './dcmodify-Cf-RPHF3.cjs';
9
9
  import { ProposedTransferSyntaxValue } from './tools.cjs';
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { L as LineSource, P as ProcessLine, a as ResultValue, b as assertUnreach
3
3
  import { A as AETitle, D as DicomTag, a as DicomTagPath, P as Port, S as SOPClassUID, T as TransferSyntaxUID, b as DicomDataset } from './DicomInstance-DGWB2mfD.js';
4
4
  export { B as Brand, C as ChangeSet, c as DicomFilePath, d as DicomInstance, e as DicomOpenOptions, W as WalkTagEntry, f as WalkTagsOptions, g as createAETitle, h as createDicomFilePath, i as createDicomTag, j as createDicomTagPath, k as createPort, l as createSOPClassUID, m as createTransferSyntaxUID, t as tag, w as walkTags } from './DicomInstance-DGWB2mfD.js';
5
5
  import { z } from 'zod';
6
- 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, _ as PoolAssociationReceivedData, $ as PoolCStoreRequestData, a0 as PoolEchoRequestData, a1 as PoolRefusingAssociationData, 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-D7nnTY1R.js';
6
+ 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, _ as PoolAssociationReceivedData, $ as PoolCStoreRequestData, a0 as PoolEchoRequestData, a1 as PoolRefusingAssociationData, 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';
7
7
  import { T as TagModification } from './dcmodify-CHvwChFu.js';
8
8
  export { b as DcmodifyOptions, c as DcmodifyResult, D as DicomJsonElement, a as DicomJsonModel, d as ToolBaseOptions, e as dcmodify, x as xmlToJson } from './dcmodify-CHvwChFu.js';
9
9
  import { ProposedTransferSyntaxValue } from './tools.js';
package/dist/index.js CHANGED
@@ -36466,6 +36466,7 @@ var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
36466
36466
  var CONNECTION_RETRY_INTERVAL_MS = 500;
36467
36467
  var MAX_CONNECTION_RETRIES = 200;
36468
36468
  var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
36469
+ var ABORT_REAP_GRACE_MS = 5e3;
36469
36470
  var DicomReceiverOptionsSchema = z.object({
36470
36471
  port: z.number().int().min(0).max(65535),
36471
36472
  storageDir: z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
@@ -36503,6 +36504,9 @@ var Worker = class {
36503
36504
  __publicField(this, "_instancePending", /* @__PURE__ */ new Set());
36504
36505
  __publicField(this, "_instancesReceived", 0);
36505
36506
  __publicField(this, "_instanceErrors", 0);
36507
+ __publicField(this, "_finalized", false);
36508
+ __publicField(this, "_associationReceivedEmitted", false);
36509
+ __publicField(this, "_reapScheduled", false);
36506
36510
  __publicField(this, "_files", []);
36507
36511
  __publicField(this, "_fileSizes", []);
36508
36512
  __publicField(this, "_outputLines", []);
@@ -36543,6 +36547,62 @@ var Worker = class {
36543
36547
  this._instancePending.clear();
36544
36548
  this._instancesReceived = 0;
36545
36549
  this._instanceErrors = 0;
36550
+ this._finalized = false;
36551
+ this._associationReceivedEmitted = false;
36552
+ this._reapScheduled = false;
36553
+ }
36554
+ /**
36555
+ * Claims the single abort-reap timer slot for this association. The socket
36556
+ * `cleanup` handler runs on up to four socket events (remote/worker ×
36557
+ * error/close, plus an explicit setup-race call), so without this guard each
36558
+ * teardown would arm its own redundant grace-period timer. Returns true only
36559
+ * for the first caller; subsequent calls return false and arm nothing.
36560
+ */
36561
+ claimReapSchedule() {
36562
+ if (this._reapScheduled) return false;
36563
+ this._reapScheduled = true;
36564
+ return true;
36565
+ }
36566
+ /** True once per association: the first caller wins the finalize, others get false. */
36567
+ beginFinalizeOnce() {
36568
+ if (this._finalized) return false;
36569
+ this._finalized = true;
36570
+ return true;
36571
+ }
36572
+ /** Whether finalize handling has already been claimed for the current association. */
36573
+ get finalized() {
36574
+ return this._finalized;
36575
+ }
36576
+ /** Records that ASSOCIATION_RECEIVED was emitted to consumers for this association. */
36577
+ markAssociationReceived() {
36578
+ this._associationReceivedEmitted = true;
36579
+ }
36580
+ /**
36581
+ * Whether ASSOCIATION_RECEIVED was emitted for the current association —
36582
+ * i.e. the association was handed off to consumers. When false (a connection
36583
+ * that aborted during/before DICOM negotiation), no consumer ever engaged
36584
+ * with the association directory, so the pool owns its cleanup.
36585
+ */
36586
+ get associationReceived() {
36587
+ return this._associationReceivedEmitted;
36588
+ }
36589
+ /**
36590
+ * Atomically reserves a just-selected idle worker so concurrent connection
36591
+ * routing cannot hand the same worker to two associations. Must be called
36592
+ * synchronously the instant an idle worker is picked — before any `await` —
36593
+ * because `handleConnection` yields (e.g. on `ensureDirectory`) between
36594
+ * selecting a worker and calling {@link beginAssociation}. Without this,
36595
+ * two concurrent connections both see the worker as idle, both proceed, and
36596
+ * the second `beginAssociation` overwrites the first's context — orphaning
36597
+ * the first association (no terminal event ever fires for it). Released via
36598
+ * {@link release} if association setup fails before `beginAssociation`.
36599
+ */
36600
+ reserve() {
36601
+ this._state = "busy";
36602
+ }
36603
+ /** Returns a reserved-but-not-yet-begun worker to the idle pool (setup failed). */
36604
+ release() {
36605
+ this._state = "idle";
36546
36606
  }
36547
36607
  /** Marks the worker as finalizing — still has valid context but cannot accept new connections. */
36548
36608
  markFinalizing() {
@@ -36928,6 +36988,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36928
36988
  const associationDir = path.join(this.options.storageDir, associationId);
36929
36989
  const mkdirResult = await ensureDirectory(associationDir);
36930
36990
  if (!mkdirResult.ok) {
36991
+ worker.release();
36931
36992
  remoteSocket.destroy();
36932
36993
  this.emit("error", { error: mkdirResult.error });
36933
36994
  return;
@@ -36946,12 +37007,18 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36946
37007
  /** Finds an idle worker, retrying up to connectionTimeoutMs. */
36947
37008
  async findIdleWorker() {
36948
37009
  const idle = this.getIdleWorker();
36949
- if (idle !== void 0) return idle;
37010
+ if (idle !== void 0) {
37011
+ idle.reserve();
37012
+ return idle;
37013
+ }
36950
37014
  const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
36951
37015
  for (let i = 0; i < maxRetries; i++) {
36952
37016
  await setTimeout$1(CONNECTION_RETRY_INTERVAL_MS);
36953
37017
  const found = this.getIdleWorker();
36954
- if (found !== void 0) return found;
37018
+ if (found !== void 0) {
37019
+ found.reserve();
37020
+ return found;
37021
+ }
36955
37022
  if (this.stopping) return void 0;
36956
37023
  }
36957
37024
  return void 0;
@@ -36967,11 +37034,13 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36967
37034
  pipeConnection(worker, remoteSocket) {
36968
37035
  const workerSocket = net.createConnection({ port: worker.port, host: "127.0.0.1" });
36969
37036
  worker.setSockets(remoteSocket, workerSocket);
37037
+ const ctx = worker.context;
36970
37038
  const cleanup = () => {
36971
37039
  remoteSocket.unpipe(workerSocket);
36972
37040
  workerSocket.unpipe(remoteSocket);
36973
37041
  if (!remoteSocket.destroyed) remoteSocket.destroy();
36974
37042
  if (!workerSocket.destroyed) workerSocket.destroy();
37043
+ this.scheduleAbortReap(worker, ctx);
36975
37044
  };
36976
37045
  workerSocket.on("connect", () => {
36977
37046
  remoteSocket.pipe(workerSocket);
@@ -36982,6 +37051,42 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36982
37051
  workerSocket.on("error", cleanup);
36983
37052
  remoteSocket.on("close", cleanup);
36984
37053
  workerSocket.on("close", cleanup);
37054
+ if (remoteSocket.destroyed) {
37055
+ cleanup();
37056
+ }
37057
+ }
37058
+ /**
37059
+ * After a connection's sockets tear down, reaps the association as an
37060
+ * aborted one *only if* dcmrecv never reported completion within
37061
+ * {@link ABORT_REAP_GRACE_MS}. Without this, a peer abort destroys the
37062
+ * sockets but dcmrecv may never emit ASSOCIATION_COMPLETE, so the worker
37063
+ * stays busy forever, ASSOCIATION_FINALIZED never fires (consumers can't
37064
+ * release per-association state), and the association directory is never
37065
+ * removed. The grace delay ensures a normal A-RELEASE — whose socket also
37066
+ * closes — is finalized by its own completion report and never reaped here.
37067
+ */
37068
+ scheduleAbortReap(worker, ctx) {
37069
+ if (ctx === void 0 || worker.context !== ctx || worker.state !== "busy") return;
37070
+ if (!worker.claimReapSchedule()) return;
37071
+ const timer = setTimeout(() => {
37072
+ if (worker.context !== ctx || worker.state !== "busy" || worker.finalized) return;
37073
+ worker.markFinalizing();
37074
+ const removeDir = !worker.associationReceived;
37075
+ void this.finalizeAssociation(
37076
+ worker,
37077
+ {
37078
+ associationId: ctx.associationId,
37079
+ callingAE: "",
37080
+ calledAE: "",
37081
+ source: "",
37082
+ files: [],
37083
+ durationMs: Date.now() - ctx.startAt,
37084
+ endReason: "abort"
37085
+ },
37086
+ removeDir
37087
+ );
37088
+ }, ABORT_REAP_GRACE_MS);
37089
+ timer.unref();
36985
37090
  }
36986
37091
  // -----------------------------------------------------------------------
36987
37092
  // Worker pool management
@@ -37180,19 +37285,38 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
37180
37285
  /** Returns worker to idle pool on association complete, emits summary. */
37181
37286
  wireAssociationComplete(worker) {
37182
37287
  worker.dcmrecv.onAssociationComplete((data) => {
37288
+ if (worker.finalized) return;
37183
37289
  worker.markFinalizing();
37290
+ const removeDir = !worker.associationReceived;
37184
37291
  setImmediate(() => {
37185
- void this.finalizeAssociation(worker, data);
37292
+ void this.finalizeAssociation(worker, data, removeDir);
37186
37293
  });
37187
37294
  });
37188
37295
  }
37189
- /** Awaits file ops, emits ASSOCIATION_COMPLETE, awaits parsing, emits ASSOCIATION_FINALIZED. */
37190
- async finalizeAssociation(worker, data) {
37296
+ /**
37297
+ * Awaits file ops, emits ASSOCIATION_COMPLETE, awaits parsing, emits
37298
+ * ASSOCIATION_FINALIZED, returns the worker to idle. Runs at most once per
37299
+ * association via {@link Worker.beginFinalizeOnce}.
37300
+ *
37301
+ * @param removeDir - When true, removes the association directory after
37302
+ * finalizing. Set when no consumer ever engaged with the association
37303
+ * (no ASSOCIATION_RECEIVED was emitted) — e.g. a connection that aborted
37304
+ * during negotiation — so the pool reclaims the directory it created.
37305
+ * When a consumer did engage this stays false: the consumer owns
37306
+ * directory cleanup once it has processed the received files (removing it
37307
+ * here could race in-flight consumer reads of those files).
37308
+ */
37309
+ async finalizeAssociation(worker, data, removeDir = false) {
37310
+ if (!worker.beginFinalizeOnce()) return;
37191
37311
  await worker.drainPendingFiles();
37192
37312
  this.emitAssociationComplete(worker, data);
37193
37313
  await worker.drainInstancePending();
37194
37314
  this.emitAssociationFinalized(worker, data);
37315
+ const associationDir = worker.context?.associationDir;
37195
37316
  worker.endAssociation();
37317
+ if (removeDir && associationDir !== void 0) {
37318
+ await removeDirSafe(associationDir);
37319
+ }
37196
37320
  void this.scaleDown();
37197
37321
  }
37198
37322
  /** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
@@ -37234,12 +37358,15 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
37234
37358
  source: data.source,
37235
37359
  files: [...worker.files],
37236
37360
  instancesReceived: worker.instancesReceived,
37237
- instanceErrors: worker.instanceErrors
37361
+ instanceErrors: worker.instanceErrors,
37362
+ endReason: data.endReason
37238
37363
  });
37239
37364
  }
37240
37365
  /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
37241
37366
  wireAssociationReceived(worker) {
37242
37367
  worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
37368
+ if (worker.finalized) return;
37369
+ worker.markAssociationReceived();
37243
37370
  this.emit("ASSOCIATION_RECEIVED", {
37244
37371
  associationId: worker.context?.associationId ?? "",
37245
37372
  callingAE: data.callingAE,
@@ -37550,7 +37677,12 @@ var SenderEngine = class {
37550
37677
  this.activeAssociations++;
37551
37678
  const startMs = Date.now();
37552
37679
  const maxAttempts = entry.maxRetries + 1;
37553
- const failure = await this.attemptSend(entry, maxAttempts, startMs);
37680
+ let failure;
37681
+ try {
37682
+ failure = await this.attemptSend(entry, maxAttempts, startMs);
37683
+ } catch (e) {
37684
+ failure = { error: e instanceof Error ? e : new Error(String(e)), output: { stdout: "", stderr: "" } };
37685
+ }
37554
37686
  if (failure === void 0) return;
37555
37687
  this.activeAssociations--;
37556
37688
  this.recordFailure();