@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
|
@@ -1567,6 +1567,14 @@ interface ReceiverAssociationFinalizedData {
|
|
|
1567
1567
|
readonly files: readonly string[];
|
|
1568
1568
|
readonly instancesReceived: number;
|
|
1569
1569
|
readonly instanceErrors: number;
|
|
1570
|
+
/**
|
|
1571
|
+
* How the association ended: `'release'` for a normal A-RELEASE, `'abort'`
|
|
1572
|
+
* for a peer abort (including associations synthesized by the grace-period
|
|
1573
|
+
* reaper when dcmrecv never reported completion). Lets consumers that drive
|
|
1574
|
+
* terminal state off this event distinguish a reaped abort from a clean
|
|
1575
|
+
* finish without also listening to ASSOCIATION_COMPLETE.
|
|
1576
|
+
*/
|
|
1577
|
+
readonly endReason: 'release' | 'abort';
|
|
1570
1578
|
}
|
|
1571
1579
|
/** Data emitted with error events. */
|
|
1572
1580
|
interface ReceiverErrorData {
|
|
@@ -1787,6 +1795,17 @@ declare class DicomReceiver extends EventEmitter<DicomReceiverEventMap> {
|
|
|
1787
1795
|
private getIdleWorker;
|
|
1788
1796
|
/** Pipes remote socket bidirectionally to the worker's port. */
|
|
1789
1797
|
private pipeConnection;
|
|
1798
|
+
/**
|
|
1799
|
+
* After a connection's sockets tear down, reaps the association as an
|
|
1800
|
+
* aborted one *only if* dcmrecv never reported completion within
|
|
1801
|
+
* {@link ABORT_REAP_GRACE_MS}. Without this, a peer abort destroys the
|
|
1802
|
+
* sockets but dcmrecv may never emit ASSOCIATION_COMPLETE, so the worker
|
|
1803
|
+
* stays busy forever, ASSOCIATION_FINALIZED never fires (consumers can't
|
|
1804
|
+
* release per-association state), and the association directory is never
|
|
1805
|
+
* removed. The grace delay ensures a normal A-RELEASE — whose socket also
|
|
1806
|
+
* closes — is finalized by its own completion report and never reaped here.
|
|
1807
|
+
*/
|
|
1808
|
+
private scheduleAbortReap;
|
|
1790
1809
|
/** Spawns `count` new workers and adds them to the pool. */
|
|
1791
1810
|
private spawnWorkers;
|
|
1792
1811
|
/** Creates a Dcmrecv instance with the pool's shared options. */
|
|
@@ -1811,7 +1830,19 @@ declare class DicomReceiver extends EventEmitter<DicomReceiverEventMap> {
|
|
|
1811
1830
|
private parseAndEmitInstance;
|
|
1812
1831
|
/** Returns worker to idle pool on association complete, emits summary. */
|
|
1813
1832
|
private wireAssociationComplete;
|
|
1814
|
-
/**
|
|
1833
|
+
/**
|
|
1834
|
+
* Awaits file ops, emits ASSOCIATION_COMPLETE, awaits parsing, emits
|
|
1835
|
+
* ASSOCIATION_FINALIZED, returns the worker to idle. Runs at most once per
|
|
1836
|
+
* association via {@link Worker.beginFinalizeOnce}.
|
|
1837
|
+
*
|
|
1838
|
+
* @param removeDir - When true, removes the association directory after
|
|
1839
|
+
* finalizing. Set when no consumer ever engaged with the association
|
|
1840
|
+
* (no ASSOCIATION_RECEIVED was emitted) — e.g. a connection that aborted
|
|
1841
|
+
* during negotiation — so the pool reclaims the directory it created.
|
|
1842
|
+
* When a consumer did engage this stays false: the consumer owns
|
|
1843
|
+
* directory cleanup once it has processed the received files (removing it
|
|
1844
|
+
* here could race in-flight consumer reads of those files).
|
|
1845
|
+
*/
|
|
1815
1846
|
private finalizeAssociation;
|
|
1816
1847
|
/** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
|
|
1817
1848
|
private emitAssociationComplete;
|
|
@@ -1567,6 +1567,14 @@ interface ReceiverAssociationFinalizedData {
|
|
|
1567
1567
|
readonly files: readonly string[];
|
|
1568
1568
|
readonly instancesReceived: number;
|
|
1569
1569
|
readonly instanceErrors: number;
|
|
1570
|
+
/**
|
|
1571
|
+
* How the association ended: `'release'` for a normal A-RELEASE, `'abort'`
|
|
1572
|
+
* for a peer abort (including associations synthesized by the grace-period
|
|
1573
|
+
* reaper when dcmrecv never reported completion). Lets consumers that drive
|
|
1574
|
+
* terminal state off this event distinguish a reaped abort from a clean
|
|
1575
|
+
* finish without also listening to ASSOCIATION_COMPLETE.
|
|
1576
|
+
*/
|
|
1577
|
+
readonly endReason: 'release' | 'abort';
|
|
1570
1578
|
}
|
|
1571
1579
|
/** Data emitted with error events. */
|
|
1572
1580
|
interface ReceiverErrorData {
|
|
@@ -1787,6 +1795,17 @@ declare class DicomReceiver extends EventEmitter<DicomReceiverEventMap> {
|
|
|
1787
1795
|
private getIdleWorker;
|
|
1788
1796
|
/** Pipes remote socket bidirectionally to the worker's port. */
|
|
1789
1797
|
private pipeConnection;
|
|
1798
|
+
/**
|
|
1799
|
+
* After a connection's sockets tear down, reaps the association as an
|
|
1800
|
+
* aborted one *only if* dcmrecv never reported completion within
|
|
1801
|
+
* {@link ABORT_REAP_GRACE_MS}. Without this, a peer abort destroys the
|
|
1802
|
+
* sockets but dcmrecv may never emit ASSOCIATION_COMPLETE, so the worker
|
|
1803
|
+
* stays busy forever, ASSOCIATION_FINALIZED never fires (consumers can't
|
|
1804
|
+
* release per-association state), and the association directory is never
|
|
1805
|
+
* removed. The grace delay ensures a normal A-RELEASE — whose socket also
|
|
1806
|
+
* closes — is finalized by its own completion report and never reaped here.
|
|
1807
|
+
*/
|
|
1808
|
+
private scheduleAbortReap;
|
|
1790
1809
|
/** Spawns `count` new workers and adds them to the pool. */
|
|
1791
1810
|
private spawnWorkers;
|
|
1792
1811
|
/** Creates a Dcmrecv instance with the pool's shared options. */
|
|
@@ -1811,7 +1830,19 @@ declare class DicomReceiver extends EventEmitter<DicomReceiverEventMap> {
|
|
|
1811
1830
|
private parseAndEmitInstance;
|
|
1812
1831
|
/** Returns worker to idle pool on association complete, emits summary. */
|
|
1813
1832
|
private wireAssociationComplete;
|
|
1814
|
-
/**
|
|
1833
|
+
/**
|
|
1834
|
+
* Awaits file ops, emits ASSOCIATION_COMPLETE, awaits parsing, emits
|
|
1835
|
+
* ASSOCIATION_FINALIZED, returns the worker to idle. Runs at most once per
|
|
1836
|
+
* association via {@link Worker.beginFinalizeOnce}.
|
|
1837
|
+
*
|
|
1838
|
+
* @param removeDir - When true, removes the association directory after
|
|
1839
|
+
* finalizing. Set when no consumer ever engaged with the association
|
|
1840
|
+
* (no ASSOCIATION_RECEIVED was emitted) — e.g. a connection that aborted
|
|
1841
|
+
* during negotiation — so the pool reclaims the directory it created.
|
|
1842
|
+
* When a consumer did engage this stays false: the consumer owns
|
|
1843
|
+
* directory cleanup once it has processed the received files (removing it
|
|
1844
|
+
* here could race in-flight consumer reads of those files).
|
|
1845
|
+
*/
|
|
1815
1846
|
private finalizeAssociation;
|
|
1816
1847
|
/** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
|
|
1817
1848
|
private emitAssociationComplete;
|
package/dist/index.cjs
CHANGED
|
@@ -31290,8 +31290,14 @@ var KNOWN_VR_CODES = /* @__PURE__ */ new Set([
|
|
|
31290
31290
|
"UT",
|
|
31291
31291
|
"UV"
|
|
31292
31292
|
]);
|
|
31293
|
+
function decodeXmlEntities(value) {
|
|
31294
|
+
return value.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
31295
|
+
}
|
|
31296
|
+
function decodeIfString(value) {
|
|
31297
|
+
return typeof value === "string" ? decodeXmlEntities(value) : value;
|
|
31298
|
+
}
|
|
31293
31299
|
function buildPnString(comp) {
|
|
31294
|
-
const parts = [comp.FamilyName ?? "", comp.GivenName ?? "", comp.MiddleName ?? "", comp.NamePrefix ?? "", comp.NameSuffix ?? ""];
|
|
31300
|
+
const parts = [comp.FamilyName ?? "", comp.GivenName ?? "", comp.MiddleName ?? "", comp.NamePrefix ?? "", comp.NameSuffix ?? ""].map(decodeXmlEntities);
|
|
31295
31301
|
let last = parts.length - 1;
|
|
31296
31302
|
for (; last >= 0; last--) {
|
|
31297
31303
|
if (parts[last] !== "") break;
|
|
@@ -31330,9 +31336,9 @@ function convertBulkDataURI(attr, element) {
|
|
|
31330
31336
|
const bulkArray = toArray(attr.BulkDataURI);
|
|
31331
31337
|
const firstBulk = bulkArray[0];
|
|
31332
31338
|
if (typeof firstBulk === "object" && firstBulk !== null && "@_uri" in firstBulk) {
|
|
31333
|
-
element.BulkDataURI = safeString(firstBulk["@_uri"]);
|
|
31339
|
+
element.BulkDataURI = decodeXmlEntities(safeString(firstBulk["@_uri"]));
|
|
31334
31340
|
} else {
|
|
31335
|
-
element.BulkDataURI = safeString(firstBulk);
|
|
31341
|
+
element.BulkDataURI = decodeXmlEntities(safeString(firstBulk));
|
|
31336
31342
|
}
|
|
31337
31343
|
}
|
|
31338
31344
|
function convertPNValue(attr, element) {
|
|
@@ -31376,7 +31382,7 @@ function convertRegularValue(attr, element, vr) {
|
|
|
31376
31382
|
const values = [];
|
|
31377
31383
|
const isNumeric = NUMERIC_JSON_VRS.has(vr);
|
|
31378
31384
|
for (const v of valArray) {
|
|
31379
|
-
const unwrapped = unwrapValue(v);
|
|
31385
|
+
const unwrapped = decodeIfString(unwrapValue(v));
|
|
31380
31386
|
values.push(isNumeric ? coerceNumeric(unwrapped) : unwrapped);
|
|
31381
31387
|
}
|
|
31382
31388
|
if (values.length > 0) element.Value = values;
|
|
@@ -36485,6 +36491,7 @@ var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
|
36485
36491
|
var CONNECTION_RETRY_INTERVAL_MS = 500;
|
|
36486
36492
|
var MAX_CONNECTION_RETRIES = 200;
|
|
36487
36493
|
var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
|
|
36494
|
+
var ABORT_REAP_GRACE_MS = 5e3;
|
|
36488
36495
|
var DicomReceiverOptionsSchema = zod.z.object({
|
|
36489
36496
|
port: zod.z.number().int().min(0).max(65535),
|
|
36490
36497
|
storageDir: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
|
|
@@ -36522,6 +36529,9 @@ var Worker = class {
|
|
|
36522
36529
|
__publicField(this, "_instancePending", /* @__PURE__ */ new Set());
|
|
36523
36530
|
__publicField(this, "_instancesReceived", 0);
|
|
36524
36531
|
__publicField(this, "_instanceErrors", 0);
|
|
36532
|
+
__publicField(this, "_finalized", false);
|
|
36533
|
+
__publicField(this, "_associationReceivedEmitted", false);
|
|
36534
|
+
__publicField(this, "_reapScheduled", false);
|
|
36525
36535
|
__publicField(this, "_files", []);
|
|
36526
36536
|
__publicField(this, "_fileSizes", []);
|
|
36527
36537
|
__publicField(this, "_outputLines", []);
|
|
@@ -36562,6 +36572,62 @@ var Worker = class {
|
|
|
36562
36572
|
this._instancePending.clear();
|
|
36563
36573
|
this._instancesReceived = 0;
|
|
36564
36574
|
this._instanceErrors = 0;
|
|
36575
|
+
this._finalized = false;
|
|
36576
|
+
this._associationReceivedEmitted = false;
|
|
36577
|
+
this._reapScheduled = false;
|
|
36578
|
+
}
|
|
36579
|
+
/**
|
|
36580
|
+
* Claims the single abort-reap timer slot for this association. The socket
|
|
36581
|
+
* `cleanup` handler runs on up to four socket events (remote/worker ×
|
|
36582
|
+
* error/close, plus an explicit setup-race call), so without this guard each
|
|
36583
|
+
* teardown would arm its own redundant grace-period timer. Returns true only
|
|
36584
|
+
* for the first caller; subsequent calls return false and arm nothing.
|
|
36585
|
+
*/
|
|
36586
|
+
claimReapSchedule() {
|
|
36587
|
+
if (this._reapScheduled) return false;
|
|
36588
|
+
this._reapScheduled = true;
|
|
36589
|
+
return true;
|
|
36590
|
+
}
|
|
36591
|
+
/** True once per association: the first caller wins the finalize, others get false. */
|
|
36592
|
+
beginFinalizeOnce() {
|
|
36593
|
+
if (this._finalized) return false;
|
|
36594
|
+
this._finalized = true;
|
|
36595
|
+
return true;
|
|
36596
|
+
}
|
|
36597
|
+
/** Whether finalize handling has already been claimed for the current association. */
|
|
36598
|
+
get finalized() {
|
|
36599
|
+
return this._finalized;
|
|
36600
|
+
}
|
|
36601
|
+
/** Records that ASSOCIATION_RECEIVED was emitted to consumers for this association. */
|
|
36602
|
+
markAssociationReceived() {
|
|
36603
|
+
this._associationReceivedEmitted = true;
|
|
36604
|
+
}
|
|
36605
|
+
/**
|
|
36606
|
+
* Whether ASSOCIATION_RECEIVED was emitted for the current association —
|
|
36607
|
+
* i.e. the association was handed off to consumers. When false (a connection
|
|
36608
|
+
* that aborted during/before DICOM negotiation), no consumer ever engaged
|
|
36609
|
+
* with the association directory, so the pool owns its cleanup.
|
|
36610
|
+
*/
|
|
36611
|
+
get associationReceived() {
|
|
36612
|
+
return this._associationReceivedEmitted;
|
|
36613
|
+
}
|
|
36614
|
+
/**
|
|
36615
|
+
* Atomically reserves a just-selected idle worker so concurrent connection
|
|
36616
|
+
* routing cannot hand the same worker to two associations. Must be called
|
|
36617
|
+
* synchronously the instant an idle worker is picked — before any `await` —
|
|
36618
|
+
* because `handleConnection` yields (e.g. on `ensureDirectory`) between
|
|
36619
|
+
* selecting a worker and calling {@link beginAssociation}. Without this,
|
|
36620
|
+
* two concurrent connections both see the worker as idle, both proceed, and
|
|
36621
|
+
* the second `beginAssociation` overwrites the first's context — orphaning
|
|
36622
|
+
* the first association (no terminal event ever fires for it). Released via
|
|
36623
|
+
* {@link release} if association setup fails before `beginAssociation`.
|
|
36624
|
+
*/
|
|
36625
|
+
reserve() {
|
|
36626
|
+
this._state = "busy";
|
|
36627
|
+
}
|
|
36628
|
+
/** Returns a reserved-but-not-yet-begun worker to the idle pool (setup failed). */
|
|
36629
|
+
release() {
|
|
36630
|
+
this._state = "idle";
|
|
36565
36631
|
}
|
|
36566
36632
|
/** Marks the worker as finalizing — still has valid context but cannot accept new connections. */
|
|
36567
36633
|
markFinalizing() {
|
|
@@ -36947,6 +37013,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
36947
37013
|
const associationDir = path__namespace.join(this.options.storageDir, associationId);
|
|
36948
37014
|
const mkdirResult = await ensureDirectory(associationDir);
|
|
36949
37015
|
if (!mkdirResult.ok) {
|
|
37016
|
+
worker.release();
|
|
36950
37017
|
remoteSocket.destroy();
|
|
36951
37018
|
this.emit("error", { error: mkdirResult.error });
|
|
36952
37019
|
return;
|
|
@@ -36965,12 +37032,18 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
36965
37032
|
/** Finds an idle worker, retrying up to connectionTimeoutMs. */
|
|
36966
37033
|
async findIdleWorker() {
|
|
36967
37034
|
const idle = this.getIdleWorker();
|
|
36968
|
-
if (idle !== void 0)
|
|
37035
|
+
if (idle !== void 0) {
|
|
37036
|
+
idle.reserve();
|
|
37037
|
+
return idle;
|
|
37038
|
+
}
|
|
36969
37039
|
const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
|
|
36970
37040
|
for (let i = 0; i < maxRetries; i++) {
|
|
36971
37041
|
await promises.setTimeout(CONNECTION_RETRY_INTERVAL_MS);
|
|
36972
37042
|
const found = this.getIdleWorker();
|
|
36973
|
-
if (found !== void 0)
|
|
37043
|
+
if (found !== void 0) {
|
|
37044
|
+
found.reserve();
|
|
37045
|
+
return found;
|
|
37046
|
+
}
|
|
36974
37047
|
if (this.stopping) return void 0;
|
|
36975
37048
|
}
|
|
36976
37049
|
return void 0;
|
|
@@ -36986,11 +37059,13 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
36986
37059
|
pipeConnection(worker, remoteSocket) {
|
|
36987
37060
|
const workerSocket = net__namespace.createConnection({ port: worker.port, host: "127.0.0.1" });
|
|
36988
37061
|
worker.setSockets(remoteSocket, workerSocket);
|
|
37062
|
+
const ctx = worker.context;
|
|
36989
37063
|
const cleanup = () => {
|
|
36990
37064
|
remoteSocket.unpipe(workerSocket);
|
|
36991
37065
|
workerSocket.unpipe(remoteSocket);
|
|
36992
37066
|
if (!remoteSocket.destroyed) remoteSocket.destroy();
|
|
36993
37067
|
if (!workerSocket.destroyed) workerSocket.destroy();
|
|
37068
|
+
this.scheduleAbortReap(worker, ctx);
|
|
36994
37069
|
};
|
|
36995
37070
|
workerSocket.on("connect", () => {
|
|
36996
37071
|
remoteSocket.pipe(workerSocket);
|
|
@@ -37001,6 +37076,42 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
37001
37076
|
workerSocket.on("error", cleanup);
|
|
37002
37077
|
remoteSocket.on("close", cleanup);
|
|
37003
37078
|
workerSocket.on("close", cleanup);
|
|
37079
|
+
if (remoteSocket.destroyed) {
|
|
37080
|
+
cleanup();
|
|
37081
|
+
}
|
|
37082
|
+
}
|
|
37083
|
+
/**
|
|
37084
|
+
* After a connection's sockets tear down, reaps the association as an
|
|
37085
|
+
* aborted one *only if* dcmrecv never reported completion within
|
|
37086
|
+
* {@link ABORT_REAP_GRACE_MS}. Without this, a peer abort destroys the
|
|
37087
|
+
* sockets but dcmrecv may never emit ASSOCIATION_COMPLETE, so the worker
|
|
37088
|
+
* stays busy forever, ASSOCIATION_FINALIZED never fires (consumers can't
|
|
37089
|
+
* release per-association state), and the association directory is never
|
|
37090
|
+
* removed. The grace delay ensures a normal A-RELEASE — whose socket also
|
|
37091
|
+
* closes — is finalized by its own completion report and never reaped here.
|
|
37092
|
+
*/
|
|
37093
|
+
scheduleAbortReap(worker, ctx) {
|
|
37094
|
+
if (ctx === void 0 || worker.context !== ctx || worker.state !== "busy") return;
|
|
37095
|
+
if (!worker.claimReapSchedule()) return;
|
|
37096
|
+
const timer = setTimeout(() => {
|
|
37097
|
+
if (worker.context !== ctx || worker.state !== "busy" || worker.finalized) return;
|
|
37098
|
+
worker.markFinalizing();
|
|
37099
|
+
const removeDir = !worker.associationReceived;
|
|
37100
|
+
void this.finalizeAssociation(
|
|
37101
|
+
worker,
|
|
37102
|
+
{
|
|
37103
|
+
associationId: ctx.associationId,
|
|
37104
|
+
callingAE: "",
|
|
37105
|
+
calledAE: "",
|
|
37106
|
+
source: "",
|
|
37107
|
+
files: [],
|
|
37108
|
+
durationMs: Date.now() - ctx.startAt,
|
|
37109
|
+
endReason: "abort"
|
|
37110
|
+
},
|
|
37111
|
+
removeDir
|
|
37112
|
+
);
|
|
37113
|
+
}, ABORT_REAP_GRACE_MS);
|
|
37114
|
+
timer.unref();
|
|
37004
37115
|
}
|
|
37005
37116
|
// -----------------------------------------------------------------------
|
|
37006
37117
|
// Worker pool management
|
|
@@ -37199,19 +37310,38 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
37199
37310
|
/** Returns worker to idle pool on association complete, emits summary. */
|
|
37200
37311
|
wireAssociationComplete(worker) {
|
|
37201
37312
|
worker.dcmrecv.onAssociationComplete((data) => {
|
|
37313
|
+
if (worker.finalized) return;
|
|
37202
37314
|
worker.markFinalizing();
|
|
37315
|
+
const removeDir = !worker.associationReceived;
|
|
37203
37316
|
setImmediate(() => {
|
|
37204
|
-
void this.finalizeAssociation(worker, data);
|
|
37317
|
+
void this.finalizeAssociation(worker, data, removeDir);
|
|
37205
37318
|
});
|
|
37206
37319
|
});
|
|
37207
37320
|
}
|
|
37208
|
-
/**
|
|
37209
|
-
|
|
37321
|
+
/**
|
|
37322
|
+
* Awaits file ops, emits ASSOCIATION_COMPLETE, awaits parsing, emits
|
|
37323
|
+
* ASSOCIATION_FINALIZED, returns the worker to idle. Runs at most once per
|
|
37324
|
+
* association via {@link Worker.beginFinalizeOnce}.
|
|
37325
|
+
*
|
|
37326
|
+
* @param removeDir - When true, removes the association directory after
|
|
37327
|
+
* finalizing. Set when no consumer ever engaged with the association
|
|
37328
|
+
* (no ASSOCIATION_RECEIVED was emitted) — e.g. a connection that aborted
|
|
37329
|
+
* during negotiation — so the pool reclaims the directory it created.
|
|
37330
|
+
* When a consumer did engage this stays false: the consumer owns
|
|
37331
|
+
* directory cleanup once it has processed the received files (removing it
|
|
37332
|
+
* here could race in-flight consumer reads of those files).
|
|
37333
|
+
*/
|
|
37334
|
+
async finalizeAssociation(worker, data, removeDir = false) {
|
|
37335
|
+
if (!worker.beginFinalizeOnce()) return;
|
|
37210
37336
|
await worker.drainPendingFiles();
|
|
37211
37337
|
this.emitAssociationComplete(worker, data);
|
|
37212
37338
|
await worker.drainInstancePending();
|
|
37213
37339
|
this.emitAssociationFinalized(worker, data);
|
|
37340
|
+
const associationDir = worker.context?.associationDir;
|
|
37214
37341
|
worker.endAssociation();
|
|
37342
|
+
if (removeDir && associationDir !== void 0) {
|
|
37343
|
+
await removeDirSafe(associationDir);
|
|
37344
|
+
}
|
|
37215
37345
|
void this.scaleDown();
|
|
37216
37346
|
}
|
|
37217
37347
|
/** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
|
|
@@ -37253,12 +37383,14 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
37253
37383
|
source: data.source,
|
|
37254
37384
|
files: [...worker.files],
|
|
37255
37385
|
instancesReceived: worker.instancesReceived,
|
|
37256
|
-
instanceErrors: worker.instanceErrors
|
|
37386
|
+
instanceErrors: worker.instanceErrors,
|
|
37387
|
+
endReason: data.endReason
|
|
37257
37388
|
});
|
|
37258
37389
|
}
|
|
37259
37390
|
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
37260
37391
|
wireAssociationReceived(worker) {
|
|
37261
37392
|
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
37393
|
+
worker.markAssociationReceived();
|
|
37262
37394
|
this.emit("ASSOCIATION_RECEIVED", {
|
|
37263
37395
|
associationId: worker.context?.associationId ?? "",
|
|
37264
37396
|
callingAE: data.callingAE,
|
|
@@ -37569,7 +37701,12 @@ var SenderEngine = class {
|
|
|
37569
37701
|
this.activeAssociations++;
|
|
37570
37702
|
const startMs = Date.now();
|
|
37571
37703
|
const maxAttempts = entry.maxRetries + 1;
|
|
37572
|
-
|
|
37704
|
+
let failure;
|
|
37705
|
+
try {
|
|
37706
|
+
failure = await this.attemptSend(entry, maxAttempts, startMs);
|
|
37707
|
+
} catch (e) {
|
|
37708
|
+
failure = { error: e instanceof Error ? e : new Error(String(e)), output: { stdout: "", stderr: "" } };
|
|
37709
|
+
}
|
|
37573
37710
|
if (failure === void 0) return;
|
|
37574
37711
|
this.activeAssociations--;
|
|
37575
37712
|
this.recordFailure();
|