@ubercode/dcmtk 0.9.4 → 0.10.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/dicom.cjs +8 -7
- package/dist/dicom.cjs.map +1 -1
- package/dist/dicom.js +8 -7
- package/dist/dicom.js.map +1 -1
- package/dist/{index-zdzuZTms.d.cts → index-CEqwLKgE.d.cts} +2 -4
- package/dist/{index-x98H349Q.d.ts → index-Hp4Ri9y5.d.ts} +2 -4
- package/dist/index.cjs +346 -292
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +333 -279
- package/dist/index.js.map +1 -1
- package/dist/retry-Aryrk1uP.d.ts +112 -0
- package/dist/retry-CJ60nZxE.d.cts +112 -0
- package/dist/servers.cjs +472 -420
- 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 +465 -413
- package/dist/servers.js.map +1 -1
- package/dist/tools.cjs +8 -7
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.js +8 -7
- package/dist/tools.js.map +1 -1
- package/dist/utils.cjs +46 -0
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +21 -94
- package/dist/utils.d.ts +21 -94
- package/dist/utils.js +43 -1
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/servers.js
CHANGED
|
@@ -7,10 +7,10 @@ import * as path from 'path';
|
|
|
7
7
|
import { join, normalize } from 'path';
|
|
8
8
|
import { existsSync } from 'fs';
|
|
9
9
|
import * as net from 'net';
|
|
10
|
-
import * as fs from 'fs/promises';
|
|
11
|
-
import { copyFile, unlink, stat, rm, mkdtemp } from 'fs/promises';
|
|
12
10
|
import * as os from 'os';
|
|
13
11
|
import { tmpdir } from 'os';
|
|
12
|
+
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
13
|
+
import { mkdir, rm, rename, copyFile, unlink, stat, mkdtemp } from 'fs/promises';
|
|
14
14
|
import { XMLParser } from 'fast-xml-parser';
|
|
15
15
|
|
|
16
16
|
var __defProp = Object.defineProperty;
|
|
@@ -1249,6 +1249,44 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
|
|
|
1249
1249
|
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1250
1250
|
}
|
|
1251
1251
|
};
|
|
1252
|
+
async function ensureDirectory(dirPath) {
|
|
1253
|
+
try {
|
|
1254
|
+
await mkdir(dirPath, { recursive: true });
|
|
1255
|
+
return ok(void 0);
|
|
1256
|
+
} catch (e) {
|
|
1257
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1258
|
+
return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async function moveFile(src, dest) {
|
|
1262
|
+
try {
|
|
1263
|
+
await rename(src, dest);
|
|
1264
|
+
return ok(void 0);
|
|
1265
|
+
} catch {
|
|
1266
|
+
try {
|
|
1267
|
+
await copyFile(src, dest);
|
|
1268
|
+
await unlink(src);
|
|
1269
|
+
return ok(void 0);
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1272
|
+
return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async function statFileSafe(filePath) {
|
|
1277
|
+
try {
|
|
1278
|
+
const s = await stat(filePath);
|
|
1279
|
+
return s.size;
|
|
1280
|
+
} catch {
|
|
1281
|
+
return 0;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
async function removeDirSafe(dirPath) {
|
|
1285
|
+
try {
|
|
1286
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1252
1290
|
function createDicomTag(input) {
|
|
1253
1291
|
if (!DICOM_TAG_PATTERN.test(input)) {
|
|
1254
1292
|
return err(new Error(`Invalid DICOM tag: "${input}". Expected format (XXXX,XXXX) where X is a hex digit`));
|
|
@@ -1312,227 +1350,6 @@ function tagPathToSegments(path2) {
|
|
|
1312
1350
|
return segments;
|
|
1313
1351
|
}
|
|
1314
1352
|
|
|
1315
|
-
// src/dicom/ChangeSet.ts
|
|
1316
|
-
var ERASE_PRIVATE_SENTINEL = "__ERASE_PRIVATE__";
|
|
1317
|
-
function isControlChar(code) {
|
|
1318
|
-
if (code <= 9) return true;
|
|
1319
|
-
if (code === 11 || code === 12) return true;
|
|
1320
|
-
if (code >= 14 && code <= 31) return true;
|
|
1321
|
-
return code === 127;
|
|
1322
|
-
}
|
|
1323
|
-
function sanitizeValue(value) {
|
|
1324
|
-
let result = "";
|
|
1325
|
-
for (let i = 0; i < value.length; i++) {
|
|
1326
|
-
const code = value.charCodeAt(i);
|
|
1327
|
-
if (!isControlChar(code)) {
|
|
1328
|
-
result += value[i];
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
return result;
|
|
1332
|
-
}
|
|
1333
|
-
function buildMergedModifications(base, other, erasures) {
|
|
1334
|
-
const merged = new Map(base);
|
|
1335
|
-
for (const [key, value] of other) {
|
|
1336
|
-
merged.set(key, value);
|
|
1337
|
-
}
|
|
1338
|
-
for (const key of erasures) {
|
|
1339
|
-
merged.delete(key);
|
|
1340
|
-
}
|
|
1341
|
-
return merged;
|
|
1342
|
-
}
|
|
1343
|
-
function applyBatchEntries(initial, entries) {
|
|
1344
|
-
const keys = Object.keys(entries);
|
|
1345
|
-
let cs = initial;
|
|
1346
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1347
|
-
const key = keys[i];
|
|
1348
|
-
if (key === void 0) continue;
|
|
1349
|
-
const value = entries[key];
|
|
1350
|
-
if (value === void 0) continue;
|
|
1351
|
-
cs = cs.setTag(key, value);
|
|
1352
|
-
}
|
|
1353
|
-
return cs;
|
|
1354
|
-
}
|
|
1355
|
-
var ChangeSet = class _ChangeSet {
|
|
1356
|
-
constructor(mods, erasures) {
|
|
1357
|
-
__publicField(this, "mods");
|
|
1358
|
-
__publicField(this, "erased");
|
|
1359
|
-
this.mods = mods;
|
|
1360
|
-
this.erased = erasures;
|
|
1361
|
-
}
|
|
1362
|
-
/** Creates an empty ChangeSet with no modifications or erasures. */
|
|
1363
|
-
static empty() {
|
|
1364
|
-
return new _ChangeSet(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Sets a tag value, returning a new ChangeSet.
|
|
1368
|
-
*
|
|
1369
|
-
* Control characters (except LF/CR) are stripped from the value.
|
|
1370
|
-
* If the tag was previously erased, it is removed from the erasure set.
|
|
1371
|
-
*
|
|
1372
|
-
* @param path - The DICOM tag path to set (e.g. `'(0010,0010)'`)
|
|
1373
|
-
* @param value - The new value for the tag
|
|
1374
|
-
* @returns A new ChangeSet with the modification applied
|
|
1375
|
-
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1376
|
-
*/
|
|
1377
|
-
setTag(path2, value) {
|
|
1378
|
-
const totalOps = this.mods.size + this.erased.size;
|
|
1379
|
-
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1380
|
-
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1381
|
-
}
|
|
1382
|
-
tagPathToSegments(path2);
|
|
1383
|
-
const sanitized = sanitizeValue(value);
|
|
1384
|
-
const newMods = new Map(this.mods);
|
|
1385
|
-
newMods.set(path2, sanitized);
|
|
1386
|
-
const newErasures = new Set(this.erased);
|
|
1387
|
-
newErasures.delete(path2);
|
|
1388
|
-
return new _ChangeSet(newMods, newErasures);
|
|
1389
|
-
}
|
|
1390
|
-
/**
|
|
1391
|
-
* Marks a tag for erasure, returning a new ChangeSet.
|
|
1392
|
-
*
|
|
1393
|
-
* If the tag was previously set, the modification is removed.
|
|
1394
|
-
*
|
|
1395
|
-
* @param path - The DICOM tag path to erase (e.g. `'(0010,0010)'`)
|
|
1396
|
-
* @returns A new ChangeSet with the erasure applied
|
|
1397
|
-
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1398
|
-
*/
|
|
1399
|
-
eraseTag(path2) {
|
|
1400
|
-
const totalOps = this.mods.size + this.erased.size;
|
|
1401
|
-
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1402
|
-
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1403
|
-
}
|
|
1404
|
-
tagPathToSegments(path2);
|
|
1405
|
-
const newMods = new Map(this.mods);
|
|
1406
|
-
newMods.delete(path2);
|
|
1407
|
-
const newErasures = new Set(this.erased);
|
|
1408
|
-
newErasures.add(path2);
|
|
1409
|
-
return new _ChangeSet(newMods, newErasures);
|
|
1410
|
-
}
|
|
1411
|
-
/**
|
|
1412
|
-
* Marks all private tags for erasure, returning a new ChangeSet.
|
|
1413
|
-
*
|
|
1414
|
-
* @returns A new ChangeSet with the erase-private flag set
|
|
1415
|
-
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1416
|
-
*/
|
|
1417
|
-
erasePrivateTags() {
|
|
1418
|
-
const totalOps = this.mods.size + this.erased.size;
|
|
1419
|
-
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1420
|
-
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1421
|
-
}
|
|
1422
|
-
const newErasures = new Set(this.erased);
|
|
1423
|
-
newErasures.add(ERASE_PRIVATE_SENTINEL);
|
|
1424
|
-
return new _ChangeSet(new Map(this.mods), newErasures);
|
|
1425
|
-
}
|
|
1426
|
-
// -----------------------------------------------------------------------
|
|
1427
|
-
// Convenience setters for common DICOM tags
|
|
1428
|
-
// -----------------------------------------------------------------------
|
|
1429
|
-
/** Sets Patient's Name (0010,0010). */
|
|
1430
|
-
setPatientName(value) {
|
|
1431
|
-
return this.setTag("(0010,0010)", value);
|
|
1432
|
-
}
|
|
1433
|
-
/** Sets Patient ID (0010,0020). */
|
|
1434
|
-
setPatientID(value) {
|
|
1435
|
-
return this.setTag("(0010,0020)", value);
|
|
1436
|
-
}
|
|
1437
|
-
/** Sets Study Date (0008,0020). */
|
|
1438
|
-
setStudyDate(value) {
|
|
1439
|
-
return this.setTag("(0008,0020)", value);
|
|
1440
|
-
}
|
|
1441
|
-
/** Sets Modality (0008,0060). */
|
|
1442
|
-
setModality(value) {
|
|
1443
|
-
return this.setTag("(0008,0060)", value);
|
|
1444
|
-
}
|
|
1445
|
-
/** Sets Accession Number (0008,0050). */
|
|
1446
|
-
setAccessionNumber(value) {
|
|
1447
|
-
return this.setTag("(0008,0050)", value);
|
|
1448
|
-
}
|
|
1449
|
-
/** Sets Study Description (0008,1030). */
|
|
1450
|
-
setStudyDescription(value) {
|
|
1451
|
-
return this.setTag("(0008,1030)", value);
|
|
1452
|
-
}
|
|
1453
|
-
/** Sets Series Description (0008,103E). */
|
|
1454
|
-
setSeriesDescription(value) {
|
|
1455
|
-
return this.setTag("(0008,103E)", value);
|
|
1456
|
-
}
|
|
1457
|
-
/** Sets Institution Name (0008,0080). */
|
|
1458
|
-
setInstitutionName(value) {
|
|
1459
|
-
return this.setTag("(0008,0080)", value);
|
|
1460
|
-
}
|
|
1461
|
-
/**
|
|
1462
|
-
* Sets multiple tags at once, returning a new ChangeSet.
|
|
1463
|
-
*
|
|
1464
|
-
* @param entries - A record of tag path → value pairs
|
|
1465
|
-
* @returns A new ChangeSet with all modifications applied
|
|
1466
|
-
*/
|
|
1467
|
-
setBatch(entries) {
|
|
1468
|
-
return applyBatchEntries(this, entries);
|
|
1469
|
-
}
|
|
1470
|
-
/** All pending tag modifications as a readonly map of path → value. */
|
|
1471
|
-
get modifications() {
|
|
1472
|
-
return this.mods;
|
|
1473
|
-
}
|
|
1474
|
-
/** All pending tag erasures as a readonly set of paths. */
|
|
1475
|
-
get erasures() {
|
|
1476
|
-
return this.erased;
|
|
1477
|
-
}
|
|
1478
|
-
/** Total number of operations (modifications + erasures) in this ChangeSet. */
|
|
1479
|
-
get operationCount() {
|
|
1480
|
-
return this.mods.size + this.erased.size;
|
|
1481
|
-
}
|
|
1482
|
-
/** Whether the ChangeSet has no modifications and no erasures. */
|
|
1483
|
-
get isEmpty() {
|
|
1484
|
-
return this.mods.size === 0 && this.erased.size === 0;
|
|
1485
|
-
}
|
|
1486
|
-
/** Whether the erase-all-private-tags flag is set. */
|
|
1487
|
-
get erasePrivate() {
|
|
1488
|
-
return this.erased.has(ERASE_PRIVATE_SENTINEL);
|
|
1489
|
-
}
|
|
1490
|
-
/**
|
|
1491
|
-
* Merges another ChangeSet into this one, returning a new ChangeSet.
|
|
1492
|
-
*
|
|
1493
|
-
* The `other` ChangeSet wins on conflicts: if the same tag is modified in both,
|
|
1494
|
-
* `other`'s value is used. Erasures from both sets are unioned. An erasure in
|
|
1495
|
-
* `other` removes a modification from `base`.
|
|
1496
|
-
*
|
|
1497
|
-
* @param other - The ChangeSet to merge in
|
|
1498
|
-
* @returns A new ChangeSet with merged modifications and erasures
|
|
1499
|
-
*/
|
|
1500
|
-
merge(other) {
|
|
1501
|
-
const mergedErasures = /* @__PURE__ */ new Set([...this.erased, ...other.erased]);
|
|
1502
|
-
const mergedMods = buildMergedModifications(this.mods, other.mods, mergedErasures);
|
|
1503
|
-
return new _ChangeSet(mergedMods, mergedErasures);
|
|
1504
|
-
}
|
|
1505
|
-
/**
|
|
1506
|
-
* Converts modifications to dcmodify-compatible TagModification array.
|
|
1507
|
-
*
|
|
1508
|
-
* @returns A readonly array of TagModification objects
|
|
1509
|
-
*/
|
|
1510
|
-
toModifications() {
|
|
1511
|
-
const result = [];
|
|
1512
|
-
for (const [tag, value] of this.mods) {
|
|
1513
|
-
result.push({ tag, value });
|
|
1514
|
-
}
|
|
1515
|
-
return result;
|
|
1516
|
-
}
|
|
1517
|
-
/**
|
|
1518
|
-
* Converts erasures to dcmodify-compatible argument strings.
|
|
1519
|
-
*
|
|
1520
|
-
* The erase-private sentinel is excluded — use {@link erasePrivate} to check
|
|
1521
|
-
* whether `-ep` should be passed.
|
|
1522
|
-
*
|
|
1523
|
-
* @returns A readonly array of tag path strings for `-e` arguments
|
|
1524
|
-
*/
|
|
1525
|
-
toErasureArgs() {
|
|
1526
|
-
const result = [];
|
|
1527
|
-
for (const path2 of this.erased) {
|
|
1528
|
-
if (path2 !== ERASE_PRIVATE_SENTINEL) {
|
|
1529
|
-
result.push(path2);
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
return result;
|
|
1533
|
-
}
|
|
1534
|
-
};
|
|
1535
|
-
|
|
1536
1353
|
// src/dicom/walkTags.ts
|
|
1537
1354
|
var DEFAULT_MAX_DEPTH = 16;
|
|
1538
1355
|
function walkTags(data, options) {
|
|
@@ -1876,83 +1693,304 @@ var DicomDataset = class _DicomDataset {
|
|
|
1876
1693
|
} catch (e) {
|
|
1877
1694
|
return err(stderr(e));
|
|
1878
1695
|
}
|
|
1879
|
-
return traversePath(this.data, segments);
|
|
1696
|
+
return traversePath(this.data, segments);
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Finds all values matching a path with wildcard `[*]` indices.
|
|
1700
|
+
*
|
|
1701
|
+
* Traverses all items in wildcard sequence positions using an iterative BFS queue.
|
|
1702
|
+
* The traversal is bounded to {@link MAX_TRAVERSAL_DEPTH} * 100 iterations (5 000).
|
|
1703
|
+
* For extremely large datasets this may truncate results silently; callers
|
|
1704
|
+
* needing completeness guarantees should verify dataset size independently.
|
|
1705
|
+
*
|
|
1706
|
+
* @param path - A branded DicomTagPath, e.g. `(0040,A730)[*].(0040,A160)`
|
|
1707
|
+
* @returns A readonly array of all matching values (may be empty)
|
|
1708
|
+
*/
|
|
1709
|
+
findValues(path2) {
|
|
1710
|
+
let segments;
|
|
1711
|
+
try {
|
|
1712
|
+
segments = tagPathToSegments(path2);
|
|
1713
|
+
} catch {
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
const result = collectWildcard(this.data, segments);
|
|
1717
|
+
return result.values;
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Walks all tags in this dataset, optionally filtering by VR and recursing into sequences.
|
|
1721
|
+
*
|
|
1722
|
+
* @param options - Optional VR filter and max depth
|
|
1723
|
+
* @returns A readonly array of all matching tag entries
|
|
1724
|
+
*/
|
|
1725
|
+
walkTags(options) {
|
|
1726
|
+
return walkTags(this.data, options);
|
|
1727
|
+
}
|
|
1728
|
+
// -----------------------------------------------------------------------
|
|
1729
|
+
// Convenience readonly getters
|
|
1730
|
+
// -----------------------------------------------------------------------
|
|
1731
|
+
/** Accession Number (0008,0050). */
|
|
1732
|
+
get accession() {
|
|
1733
|
+
return this.getString(TAGS.AccessionNumber);
|
|
1734
|
+
}
|
|
1735
|
+
/** Patient's Name (0010,0010). */
|
|
1736
|
+
get patientName() {
|
|
1737
|
+
return this.getString(TAGS.PatientName);
|
|
1738
|
+
}
|
|
1739
|
+
/** Patient ID (0010,0020). */
|
|
1740
|
+
get patientID() {
|
|
1741
|
+
return this.getString(TAGS.PatientID);
|
|
1742
|
+
}
|
|
1743
|
+
/** Study Date (0008,0020). */
|
|
1744
|
+
get studyDate() {
|
|
1745
|
+
return this.getString(TAGS.StudyDate);
|
|
1746
|
+
}
|
|
1747
|
+
/** Modality (0008,0060). */
|
|
1748
|
+
get modality() {
|
|
1749
|
+
return this.getString(TAGS.Modality);
|
|
1750
|
+
}
|
|
1751
|
+
/** SOP Class UID (0008,0016) as a branded SOPClassUID, or undefined if missing/invalid. */
|
|
1752
|
+
get sopClassUID() {
|
|
1753
|
+
const uid = this.getString(TAGS.SOPClassUID);
|
|
1754
|
+
if (uid.length === 0) return void 0;
|
|
1755
|
+
const result = createSOPClassUID(uid);
|
|
1756
|
+
return result.ok ? result.value : void 0;
|
|
1757
|
+
}
|
|
1758
|
+
/** Study Instance UID (0020,000D). */
|
|
1759
|
+
get studyInstanceUID() {
|
|
1760
|
+
return this.getString(TAGS.StudyInstanceUID);
|
|
1761
|
+
}
|
|
1762
|
+
/** Series Instance UID (0020,000E). */
|
|
1763
|
+
get seriesInstanceUID() {
|
|
1764
|
+
return this.getString(TAGS.SeriesInstanceUID);
|
|
1765
|
+
}
|
|
1766
|
+
/** SOP Instance UID (0008,0018). */
|
|
1767
|
+
get sopInstanceUID() {
|
|
1768
|
+
return this.getString(TAGS.SOPInstanceUID);
|
|
1769
|
+
}
|
|
1770
|
+
/** Transfer Syntax UID (0002,0010). */
|
|
1771
|
+
get transferSyntaxUID() {
|
|
1772
|
+
return this.getString(TAGS.TransferSyntaxUID);
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
// src/dicom/ChangeSet.ts
|
|
1777
|
+
var ERASE_PRIVATE_SENTINEL = "__ERASE_PRIVATE__";
|
|
1778
|
+
function isControlChar(code) {
|
|
1779
|
+
if (code <= 9) return true;
|
|
1780
|
+
if (code === 11 || code === 12) return true;
|
|
1781
|
+
if (code >= 14 && code <= 31) return true;
|
|
1782
|
+
return code === 127;
|
|
1783
|
+
}
|
|
1784
|
+
function sanitizeValue(value) {
|
|
1785
|
+
let result = "";
|
|
1786
|
+
for (let i = 0; i < value.length; i++) {
|
|
1787
|
+
const code = value.charCodeAt(i);
|
|
1788
|
+
if (!isControlChar(code)) {
|
|
1789
|
+
result += value[i];
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return result;
|
|
1793
|
+
}
|
|
1794
|
+
function buildMergedModifications(base, other, erasures) {
|
|
1795
|
+
const merged = new Map(base);
|
|
1796
|
+
for (const [key, value] of other) {
|
|
1797
|
+
merged.set(key, value);
|
|
1798
|
+
}
|
|
1799
|
+
for (const key of erasures) {
|
|
1800
|
+
merged.delete(key);
|
|
1801
|
+
}
|
|
1802
|
+
return merged;
|
|
1803
|
+
}
|
|
1804
|
+
function applyBatchEntries(initial, entries) {
|
|
1805
|
+
const keys = Object.keys(entries);
|
|
1806
|
+
let cs = initial;
|
|
1807
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1808
|
+
const key = keys[i];
|
|
1809
|
+
if (key === void 0) continue;
|
|
1810
|
+
const value = entries[key];
|
|
1811
|
+
if (value === void 0) continue;
|
|
1812
|
+
cs = cs.setTag(key, value);
|
|
1813
|
+
}
|
|
1814
|
+
return cs;
|
|
1815
|
+
}
|
|
1816
|
+
var ChangeSet = class _ChangeSet {
|
|
1817
|
+
constructor(mods, erasures) {
|
|
1818
|
+
__publicField(this, "mods");
|
|
1819
|
+
__publicField(this, "erased");
|
|
1820
|
+
this.mods = mods;
|
|
1821
|
+
this.erased = erasures;
|
|
1822
|
+
}
|
|
1823
|
+
/** Creates an empty ChangeSet with no modifications or erasures. */
|
|
1824
|
+
static empty() {
|
|
1825
|
+
return new _ChangeSet(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Sets a tag value, returning a new ChangeSet.
|
|
1829
|
+
*
|
|
1830
|
+
* Control characters (except LF/CR) are stripped from the value.
|
|
1831
|
+
* If the tag was previously erased, it is removed from the erasure set.
|
|
1832
|
+
*
|
|
1833
|
+
* @param path - The DICOM tag path to set (e.g. `'(0010,0010)'`)
|
|
1834
|
+
* @param value - The new value for the tag
|
|
1835
|
+
* @returns A new ChangeSet with the modification applied
|
|
1836
|
+
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1837
|
+
*/
|
|
1838
|
+
setTag(path2, value) {
|
|
1839
|
+
const totalOps = this.mods.size + this.erased.size;
|
|
1840
|
+
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1841
|
+
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1842
|
+
}
|
|
1843
|
+
tagPathToSegments(path2);
|
|
1844
|
+
const sanitized = sanitizeValue(value);
|
|
1845
|
+
const newMods = new Map(this.mods);
|
|
1846
|
+
newMods.set(path2, sanitized);
|
|
1847
|
+
const newErasures = new Set(this.erased);
|
|
1848
|
+
newErasures.delete(path2);
|
|
1849
|
+
return new _ChangeSet(newMods, newErasures);
|
|
1880
1850
|
}
|
|
1881
1851
|
/**
|
|
1882
|
-
*
|
|
1852
|
+
* Marks a tag for erasure, returning a new ChangeSet.
|
|
1883
1853
|
*
|
|
1884
|
-
*
|
|
1885
|
-
* The traversal is bounded to {@link MAX_TRAVERSAL_DEPTH} * 100 iterations (5 000).
|
|
1886
|
-
* For extremely large datasets this may truncate results silently; callers
|
|
1887
|
-
* needing completeness guarantees should verify dataset size independently.
|
|
1854
|
+
* If the tag was previously set, the modification is removed.
|
|
1888
1855
|
*
|
|
1889
|
-
* @param path -
|
|
1890
|
-
* @returns A
|
|
1856
|
+
* @param path - The DICOM tag path to erase (e.g. `'(0010,0010)'`)
|
|
1857
|
+
* @returns A new ChangeSet with the erasure applied
|
|
1858
|
+
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1891
1859
|
*/
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
} catch {
|
|
1897
|
-
return [];
|
|
1860
|
+
eraseTag(path2) {
|
|
1861
|
+
const totalOps = this.mods.size + this.erased.size;
|
|
1862
|
+
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1863
|
+
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1898
1864
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1865
|
+
tagPathToSegments(path2);
|
|
1866
|
+
const newMods = new Map(this.mods);
|
|
1867
|
+
newMods.delete(path2);
|
|
1868
|
+
const newErasures = new Set(this.erased);
|
|
1869
|
+
newErasures.add(path2);
|
|
1870
|
+
return new _ChangeSet(newMods, newErasures);
|
|
1901
1871
|
}
|
|
1902
1872
|
/**
|
|
1903
|
-
*
|
|
1873
|
+
* Marks all private tags for erasure, returning a new ChangeSet.
|
|
1904
1874
|
*
|
|
1905
|
-
* @
|
|
1906
|
-
* @
|
|
1875
|
+
* @returns A new ChangeSet with the erase-private flag set
|
|
1876
|
+
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1907
1877
|
*/
|
|
1908
|
-
|
|
1909
|
-
|
|
1878
|
+
erasePrivateTags() {
|
|
1879
|
+
const totalOps = this.mods.size + this.erased.size;
|
|
1880
|
+
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1881
|
+
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1882
|
+
}
|
|
1883
|
+
const newErasures = new Set(this.erased);
|
|
1884
|
+
newErasures.add(ERASE_PRIVATE_SENTINEL);
|
|
1885
|
+
return new _ChangeSet(new Map(this.mods), newErasures);
|
|
1910
1886
|
}
|
|
1911
1887
|
// -----------------------------------------------------------------------
|
|
1912
|
-
// Convenience
|
|
1888
|
+
// Convenience setters for common DICOM tags
|
|
1913
1889
|
// -----------------------------------------------------------------------
|
|
1914
|
-
/**
|
|
1915
|
-
|
|
1916
|
-
return this.
|
|
1890
|
+
/** Sets Patient's Name (0010,0010). */
|
|
1891
|
+
setPatientName(value) {
|
|
1892
|
+
return this.setTag("(0010,0010)", value);
|
|
1917
1893
|
}
|
|
1918
|
-
/** Patient
|
|
1919
|
-
|
|
1920
|
-
return this.
|
|
1894
|
+
/** Sets Patient ID (0010,0020). */
|
|
1895
|
+
setPatientID(value) {
|
|
1896
|
+
return this.setTag("(0010,0020)", value);
|
|
1921
1897
|
}
|
|
1922
|
-
/**
|
|
1923
|
-
|
|
1924
|
-
return this.
|
|
1898
|
+
/** Sets Study Date (0008,0020). */
|
|
1899
|
+
setStudyDate(value) {
|
|
1900
|
+
return this.setTag("(0008,0020)", value);
|
|
1925
1901
|
}
|
|
1926
|
-
/**
|
|
1927
|
-
|
|
1928
|
-
return this.
|
|
1902
|
+
/** Sets Modality (0008,0060). */
|
|
1903
|
+
setModality(value) {
|
|
1904
|
+
return this.setTag("(0008,0060)", value);
|
|
1929
1905
|
}
|
|
1930
|
-
/**
|
|
1931
|
-
|
|
1932
|
-
return this.
|
|
1906
|
+
/** Sets Accession Number (0008,0050). */
|
|
1907
|
+
setAccessionNumber(value) {
|
|
1908
|
+
return this.setTag("(0008,0050)", value);
|
|
1933
1909
|
}
|
|
1934
|
-
/**
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
if (uid.length === 0) return void 0;
|
|
1938
|
-
const result = createSOPClassUID(uid);
|
|
1939
|
-
return result.ok ? result.value : void 0;
|
|
1910
|
+
/** Sets Study Description (0008,1030). */
|
|
1911
|
+
setStudyDescription(value) {
|
|
1912
|
+
return this.setTag("(0008,1030)", value);
|
|
1940
1913
|
}
|
|
1941
|
-
/**
|
|
1942
|
-
|
|
1943
|
-
return this.
|
|
1914
|
+
/** Sets Series Description (0008,103E). */
|
|
1915
|
+
setSeriesDescription(value) {
|
|
1916
|
+
return this.setTag("(0008,103E)", value);
|
|
1944
1917
|
}
|
|
1945
|
-
/**
|
|
1946
|
-
|
|
1947
|
-
return this.
|
|
1918
|
+
/** Sets Institution Name (0008,0080). */
|
|
1919
|
+
setInstitutionName(value) {
|
|
1920
|
+
return this.setTag("(0008,0080)", value);
|
|
1948
1921
|
}
|
|
1949
|
-
/**
|
|
1950
|
-
|
|
1951
|
-
|
|
1922
|
+
/**
|
|
1923
|
+
* Sets multiple tags at once, returning a new ChangeSet.
|
|
1924
|
+
*
|
|
1925
|
+
* @param entries - A record of tag path → value pairs
|
|
1926
|
+
* @returns A new ChangeSet with all modifications applied
|
|
1927
|
+
*/
|
|
1928
|
+
setBatch(entries) {
|
|
1929
|
+
return applyBatchEntries(this, entries);
|
|
1952
1930
|
}
|
|
1953
|
-
/**
|
|
1954
|
-
get
|
|
1955
|
-
return this.
|
|
1931
|
+
/** All pending tag modifications as a readonly map of path → value. */
|
|
1932
|
+
get modifications() {
|
|
1933
|
+
return this.mods;
|
|
1934
|
+
}
|
|
1935
|
+
/** All pending tag erasures as a readonly set of paths. */
|
|
1936
|
+
get erasures() {
|
|
1937
|
+
return this.erased;
|
|
1938
|
+
}
|
|
1939
|
+
/** Total number of operations (modifications + erasures) in this ChangeSet. */
|
|
1940
|
+
get operationCount() {
|
|
1941
|
+
return this.mods.size + this.erased.size;
|
|
1942
|
+
}
|
|
1943
|
+
/** Whether the ChangeSet has no modifications and no erasures. */
|
|
1944
|
+
get isEmpty() {
|
|
1945
|
+
return this.mods.size === 0 && this.erased.size === 0;
|
|
1946
|
+
}
|
|
1947
|
+
/** Whether the erase-all-private-tags flag is set. */
|
|
1948
|
+
get erasePrivate() {
|
|
1949
|
+
return this.erased.has(ERASE_PRIVATE_SENTINEL);
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Merges another ChangeSet into this one, returning a new ChangeSet.
|
|
1953
|
+
*
|
|
1954
|
+
* The `other` ChangeSet wins on conflicts: if the same tag is modified in both,
|
|
1955
|
+
* `other`'s value is used. Erasures from both sets are unioned. An erasure in
|
|
1956
|
+
* `other` removes a modification from `base`.
|
|
1957
|
+
*
|
|
1958
|
+
* @param other - The ChangeSet to merge in
|
|
1959
|
+
* @returns A new ChangeSet with merged modifications and erasures
|
|
1960
|
+
*/
|
|
1961
|
+
merge(other) {
|
|
1962
|
+
const mergedErasures = /* @__PURE__ */ new Set([...this.erased, ...other.erased]);
|
|
1963
|
+
const mergedMods = buildMergedModifications(this.mods, other.mods, mergedErasures);
|
|
1964
|
+
return new _ChangeSet(mergedMods, mergedErasures);
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Converts modifications to dcmodify-compatible TagModification array.
|
|
1968
|
+
*
|
|
1969
|
+
* @returns A readonly array of TagModification objects
|
|
1970
|
+
*/
|
|
1971
|
+
toModifications() {
|
|
1972
|
+
const result = [];
|
|
1973
|
+
for (const [tag, value] of this.mods) {
|
|
1974
|
+
result.push({ tag, value });
|
|
1975
|
+
}
|
|
1976
|
+
return result;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Converts erasures to dcmodify-compatible argument strings.
|
|
1980
|
+
*
|
|
1981
|
+
* The erase-private sentinel is excluded — use {@link erasePrivate} to check
|
|
1982
|
+
* whether `-ep` should be passed.
|
|
1983
|
+
*
|
|
1984
|
+
* @returns A readonly array of tag path strings for `-e` arguments
|
|
1985
|
+
*/
|
|
1986
|
+
toErasureArgs() {
|
|
1987
|
+
const result = [];
|
|
1988
|
+
for (const path2 of this.erased) {
|
|
1989
|
+
if (path2 !== ERASE_PRIVATE_SENTINEL) {
|
|
1990
|
+
result.push(path2);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return result;
|
|
1956
1994
|
}
|
|
1957
1995
|
};
|
|
1958
1996
|
function killTree(pid) {
|
|
@@ -1969,13 +2007,14 @@ async function execCommand(binary, args, options) {
|
|
|
1969
2007
|
windowsHide: true,
|
|
1970
2008
|
signal: options?.signal
|
|
1971
2009
|
});
|
|
1972
|
-
wireSpawnListeners(child, timeoutMs, resolve);
|
|
2010
|
+
wireSpawnListeners(binary, child, timeoutMs, resolve);
|
|
1973
2011
|
});
|
|
1974
2012
|
}
|
|
1975
|
-
function wireSpawnListeners(child, timeoutMs, resolve) {
|
|
2013
|
+
function wireSpawnListeners(binary, child, timeoutMs, resolve) {
|
|
1976
2014
|
let stdout = "";
|
|
1977
2015
|
let stderr6 = "";
|
|
1978
2016
|
let settled = false;
|
|
2017
|
+
const name = binary.split("/").pop() ?? binary;
|
|
1979
2018
|
const settle = (result) => {
|
|
1980
2019
|
if (settled) return;
|
|
1981
2020
|
settled = true;
|
|
@@ -1984,25 +2023,25 @@ function wireSpawnListeners(child, timeoutMs, resolve) {
|
|
|
1984
2023
|
};
|
|
1985
2024
|
const timer = setTimeout(() => {
|
|
1986
2025
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
1987
|
-
settle(err(new Error(
|
|
2026
|
+
settle(err(new Error(`${name} timed out after ${timeoutMs}ms (pid: ${String(child.pid ?? "unknown")})`)));
|
|
1988
2027
|
}, timeoutMs);
|
|
1989
2028
|
child.stdout?.on("data", (chunk) => {
|
|
1990
2029
|
stdout += String(chunk);
|
|
1991
2030
|
if (stdout.length + stderr6.length > MAX_OUTPUT_BYTES) {
|
|
1992
2031
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
1993
|
-
settle(err(new Error(
|
|
2032
|
+
settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
|
|
1994
2033
|
}
|
|
1995
2034
|
});
|
|
1996
2035
|
child.stderr?.on("data", (chunk) => {
|
|
1997
2036
|
stderr6 += String(chunk);
|
|
1998
2037
|
if (stdout.length + stderr6.length > MAX_OUTPUT_BYTES) {
|
|
1999
2038
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
2000
|
-
settle(err(new Error(
|
|
2039
|
+
settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
|
|
2001
2040
|
}
|
|
2002
2041
|
});
|
|
2003
2042
|
child.on("error", (error) => {
|
|
2004
2043
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
2005
|
-
settle(err(new Error(
|
|
2044
|
+
settle(err(new Error(`${name} error: ${error.message}`)));
|
|
2006
2045
|
});
|
|
2007
2046
|
child.on("close", (code) => {
|
|
2008
2047
|
settle(ok({ stdout, stderr: stderr6, exitCode: code ?? 1 }));
|
|
@@ -2017,7 +2056,7 @@ async function spawnCommand(binary, args, options) {
|
|
|
2017
2056
|
windowsHide: true,
|
|
2018
2057
|
signal: options?.signal
|
|
2019
2058
|
});
|
|
2020
|
-
wireSpawnListeners(child, timeoutMs, resolve);
|
|
2059
|
+
wireSpawnListeners(binary, child, timeoutMs, resolve);
|
|
2021
2060
|
});
|
|
2022
2061
|
}
|
|
2023
2062
|
var PN_REPS = ["Alphabetic", "Ideographic", "Phonetic"];
|
|
@@ -2783,6 +2822,102 @@ var DicomReceiverOptionsSchema = z.object({
|
|
|
2783
2822
|
}).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
|
|
2784
2823
|
message: "minPoolSize must be <= maxPoolSize"
|
|
2785
2824
|
});
|
|
2825
|
+
var Worker = class {
|
|
2826
|
+
constructor(dcmrecv, port, tempDir) {
|
|
2827
|
+
__publicField(this, "dcmrecv");
|
|
2828
|
+
__publicField(this, "port");
|
|
2829
|
+
__publicField(this, "tempDir");
|
|
2830
|
+
__publicField(this, "_state", "idle");
|
|
2831
|
+
__publicField(this, "_context");
|
|
2832
|
+
__publicField(this, "_pending", /* @__PURE__ */ new Set());
|
|
2833
|
+
__publicField(this, "_files", []);
|
|
2834
|
+
__publicField(this, "_fileSizes", []);
|
|
2835
|
+
__publicField(this, "_outputLines", []);
|
|
2836
|
+
__publicField(this, "_remoteSocket");
|
|
2837
|
+
__publicField(this, "_workerSocket");
|
|
2838
|
+
this.dcmrecv = dcmrecv;
|
|
2839
|
+
this.port = port;
|
|
2840
|
+
this.tempDir = tempDir;
|
|
2841
|
+
}
|
|
2842
|
+
/** Current worker state. */
|
|
2843
|
+
get state() {
|
|
2844
|
+
return this._state;
|
|
2845
|
+
}
|
|
2846
|
+
/** Active association context, or undefined when idle. */
|
|
2847
|
+
get context() {
|
|
2848
|
+
return this._context;
|
|
2849
|
+
}
|
|
2850
|
+
/** Files moved to the association directory during the current association. */
|
|
2851
|
+
get files() {
|
|
2852
|
+
return this._files;
|
|
2853
|
+
}
|
|
2854
|
+
/** Byte sizes parallel to files[], for transfer stats. */
|
|
2855
|
+
get fileSizes() {
|
|
2856
|
+
return this._fileSizes;
|
|
2857
|
+
}
|
|
2858
|
+
/** Captured output lines during the current association. */
|
|
2859
|
+
get outputLines() {
|
|
2860
|
+
return this._outputLines;
|
|
2861
|
+
}
|
|
2862
|
+
/** Marks the worker busy with a new association context. */
|
|
2863
|
+
beginAssociation(ctx) {
|
|
2864
|
+
this._state = "busy";
|
|
2865
|
+
this._context = ctx;
|
|
2866
|
+
this._files.length = 0;
|
|
2867
|
+
this._fileSizes.length = 0;
|
|
2868
|
+
this._outputLines.length = 0;
|
|
2869
|
+
this._pending.clear();
|
|
2870
|
+
}
|
|
2871
|
+
/** Returns the worker to idle and clears all association state. */
|
|
2872
|
+
endAssociation() {
|
|
2873
|
+
this._state = "idle";
|
|
2874
|
+
this._context = void 0;
|
|
2875
|
+
this._files.length = 0;
|
|
2876
|
+
this._fileSizes.length = 0;
|
|
2877
|
+
this._outputLines.length = 0;
|
|
2878
|
+
this._pending.clear();
|
|
2879
|
+
this._remoteSocket = void 0;
|
|
2880
|
+
this._workerSocket = void 0;
|
|
2881
|
+
}
|
|
2882
|
+
/** Tracks an in-flight file handling promise; auto-removes on completion. */
|
|
2883
|
+
trackFile(promise) {
|
|
2884
|
+
this._pending.add(promise);
|
|
2885
|
+
void promise.finally(() => {
|
|
2886
|
+
this._pending.delete(promise);
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
/** Records a successfully received file path and its size. */
|
|
2890
|
+
recordFile(filePath, size) {
|
|
2891
|
+
this._files.push(filePath);
|
|
2892
|
+
this._fileSizes.push(size);
|
|
2893
|
+
}
|
|
2894
|
+
/** Awaits all in-flight file handling promises. */
|
|
2895
|
+
async drainPendingFiles() {
|
|
2896
|
+
if (this._pending.size > 0) {
|
|
2897
|
+
await Promise.all(this._pending);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
/** Appends a line to the output buffer, respecting the cap. */
|
|
2901
|
+
captureOutput(text) {
|
|
2902
|
+
if (this._outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
|
|
2903
|
+
this._outputLines.push(text);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
/** Stores the remote and worker sockets for later cleanup. */
|
|
2907
|
+
setSockets(remote, worker) {
|
|
2908
|
+
this._remoteSocket = remote;
|
|
2909
|
+
this._workerSocket = worker;
|
|
2910
|
+
}
|
|
2911
|
+
/** Destroys both sockets if they exist and are not already destroyed. */
|
|
2912
|
+
destroySockets() {
|
|
2913
|
+
if (this._remoteSocket !== void 0 && !this._remoteSocket.destroyed) {
|
|
2914
|
+
this._remoteSocket.destroy();
|
|
2915
|
+
}
|
|
2916
|
+
if (this._workerSocket !== void 0 && !this._workerSocket.destroyed) {
|
|
2917
|
+
this._workerSocket.destroy();
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2786
2921
|
function allocatePort() {
|
|
2787
2922
|
return new Promise((resolve) => {
|
|
2788
2923
|
const server = net.createServer();
|
|
@@ -2800,6 +2935,13 @@ function allocatePort() {
|
|
|
2800
2935
|
});
|
|
2801
2936
|
});
|
|
2802
2937
|
}
|
|
2938
|
+
function sumArray(arr) {
|
|
2939
|
+
let total = 0;
|
|
2940
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2941
|
+
total += arr[i] ?? 0;
|
|
2942
|
+
}
|
|
2943
|
+
return total;
|
|
2944
|
+
}
|
|
2803
2945
|
var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
2804
2946
|
constructor(options) {
|
|
2805
2947
|
super();
|
|
@@ -2814,8 +2956,6 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
2814
2956
|
__publicField(this, "stopping", false);
|
|
2815
2957
|
__publicField(this, "abortHandler");
|
|
2816
2958
|
this.setMaxListeners(20);
|
|
2817
|
-
this.on("error", () => {
|
|
2818
|
-
});
|
|
2819
2959
|
this.options = options;
|
|
2820
2960
|
this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
|
|
2821
2961
|
this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
|
|
@@ -3023,14 +3163,12 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3023
3163
|
this.emit("error", { error: mkdirResult.error });
|
|
3024
3164
|
return;
|
|
3025
3165
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
worker.
|
|
3032
|
-
worker.pendingFiles = [];
|
|
3033
|
-
worker.startAt = Date.now();
|
|
3166
|
+
const ctx = {
|
|
3167
|
+
associationId,
|
|
3168
|
+
associationDir,
|
|
3169
|
+
startAt: Date.now()
|
|
3170
|
+
};
|
|
3171
|
+
worker.beginAssociation(ctx);
|
|
3034
3172
|
this.pipeConnection(worker, remoteSocket);
|
|
3035
3173
|
void this.replenishPool();
|
|
3036
3174
|
}
|
|
@@ -3040,7 +3178,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3040
3178
|
if (idle !== void 0) return idle;
|
|
3041
3179
|
const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
|
|
3042
3180
|
for (let i = 0; i < maxRetries; i++) {
|
|
3043
|
-
await
|
|
3181
|
+
await setTimeout$1(CONNECTION_RETRY_INTERVAL_MS);
|
|
3044
3182
|
const found = this.getIdleWorker();
|
|
3045
3183
|
if (found !== void 0) return found;
|
|
3046
3184
|
if (this.stopping) return void 0;
|
|
@@ -3057,8 +3195,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3057
3195
|
/** Pipes remote socket bidirectionally to the worker's port. */
|
|
3058
3196
|
pipeConnection(worker, remoteSocket) {
|
|
3059
3197
|
const workerSocket = net.createConnection({ port: worker.port, host: "127.0.0.1" });
|
|
3060
|
-
worker.remoteSocket
|
|
3061
|
-
worker.workerSocket = workerSocket;
|
|
3198
|
+
worker.setSockets(remoteSocket, workerSocket);
|
|
3062
3199
|
const cleanup = () => {
|
|
3063
3200
|
remoteSocket.unpipe(workerSocket);
|
|
3064
3201
|
workerSocket.unpipe(remoteSocket);
|
|
@@ -3117,21 +3254,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3117
3254
|
const createResult = this.createDcmrecv(port, tempDir);
|
|
3118
3255
|
if (!createResult.ok) return createResult;
|
|
3119
3256
|
const dcmrecv = createResult.value;
|
|
3120
|
-
const worker =
|
|
3121
|
-
dcmrecv,
|
|
3122
|
-
port,
|
|
3123
|
-
tempDir,
|
|
3124
|
-
state: "idle",
|
|
3125
|
-
associationId: void 0,
|
|
3126
|
-
associationDir: void 0,
|
|
3127
|
-
files: [],
|
|
3128
|
-
fileSizes: [],
|
|
3129
|
-
outputLines: [],
|
|
3130
|
-
pendingFiles: [],
|
|
3131
|
-
startAt: void 0,
|
|
3132
|
-
remoteSocket: void 0,
|
|
3133
|
-
workerSocket: void 0
|
|
3134
|
-
};
|
|
3257
|
+
const worker = new Worker(dcmrecv, port, tempDir);
|
|
3135
3258
|
this.wireWorkerEvents(worker);
|
|
3136
3259
|
const startResult = await dcmrecv.start();
|
|
3137
3260
|
if (!startResult.ok) {
|
|
@@ -3140,14 +3263,9 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3140
3263
|
this.workers.set(port, worker);
|
|
3141
3264
|
return ok(worker);
|
|
3142
3265
|
}
|
|
3143
|
-
/** Stops a single worker: stop process, clean temp dir, remove from pool. */
|
|
3266
|
+
/** Stops a single worker: destroy sockets, stop process, clean temp dir, remove from pool. */
|
|
3144
3267
|
async stopWorker(worker) {
|
|
3145
|
-
|
|
3146
|
-
worker.remoteSocket.destroy();
|
|
3147
|
-
}
|
|
3148
|
-
if (worker.workerSocket !== void 0 && !worker.workerSocket.destroyed) {
|
|
3149
|
-
worker.workerSocket.destroy();
|
|
3150
|
-
}
|
|
3268
|
+
worker.destroySockets();
|
|
3151
3269
|
await worker.dcmrecv.stop();
|
|
3152
3270
|
worker.dcmrecv[Symbol.dispose]();
|
|
3153
3271
|
await removeDirSafe(worker.tempDir);
|
|
@@ -3199,12 +3317,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3199
3317
|
this.wireRefusingAssociation(worker);
|
|
3200
3318
|
this.wireOutputCapture(worker);
|
|
3201
3319
|
}
|
|
3202
|
-
/** Wires FILE_RECEIVED from dcmrecv worker —
|
|
3320
|
+
/** Wires FILE_RECEIVED from dcmrecv worker — captures context synchronously. */
|
|
3203
3321
|
wireFileReceived(worker) {
|
|
3204
3322
|
worker.dcmrecv.onFileReceived((data) => {
|
|
3205
|
-
const
|
|
3206
|
-
|
|
3207
|
-
if (assocDir === void 0 || assocId === void 0) {
|
|
3323
|
+
const ctx = worker.context;
|
|
3324
|
+
if (ctx === void 0) {
|
|
3208
3325
|
this.emit("error", {
|
|
3209
3326
|
error: new Error(`DicomReceiver: FILE_RECEIVED with no active association (worker state: ${worker.state})`),
|
|
3210
3327
|
filePath: data.filePath,
|
|
@@ -3214,21 +3331,21 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3214
3331
|
});
|
|
3215
3332
|
return;
|
|
3216
3333
|
}
|
|
3217
|
-
const promise = this.handleFileReceived(worker, data,
|
|
3218
|
-
worker.
|
|
3334
|
+
const promise = this.handleFileReceived(worker, data, ctx);
|
|
3335
|
+
worker.trackFile(promise);
|
|
3219
3336
|
});
|
|
3220
3337
|
}
|
|
3221
3338
|
/** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
|
|
3222
|
-
async handleFileReceived(worker, data,
|
|
3339
|
+
async handleFileReceived(worker, data, ctx) {
|
|
3223
3340
|
try {
|
|
3224
|
-
await this.moveAndEmitFile(worker, data,
|
|
3341
|
+
await this.moveAndEmitFile(worker, data, ctx);
|
|
3225
3342
|
} catch (thrown) {
|
|
3226
3343
|
const error = thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
3227
3344
|
this.emit("error", {
|
|
3228
3345
|
error,
|
|
3229
3346
|
filePath: data.filePath,
|
|
3230
|
-
associationId:
|
|
3231
|
-
associationDir:
|
|
3347
|
+
associationId: ctx.associationId,
|
|
3348
|
+
associationDir: ctx.associationDir,
|
|
3232
3349
|
callingAE: data.callingAE,
|
|
3233
3350
|
calledAE: data.calledAE,
|
|
3234
3351
|
source: data.source
|
|
@@ -3236,21 +3353,20 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3236
3353
|
}
|
|
3237
3354
|
}
|
|
3238
3355
|
/** Inner handler: move file, parse DICOM, emit FILE_RECEIVED or error. */
|
|
3239
|
-
async moveAndEmitFile(worker, data,
|
|
3356
|
+
async moveAndEmitFile(worker, data, ctx) {
|
|
3240
3357
|
const srcPath = data.filePath;
|
|
3241
|
-
const destPath = path.join(
|
|
3358
|
+
const destPath = path.join(ctx.associationDir, path.basename(srcPath));
|
|
3242
3359
|
const moveResult = await moveFile(srcPath, destPath);
|
|
3243
3360
|
const finalPath = moveResult.ok ? destPath : srcPath;
|
|
3244
3361
|
const fileSize = await statFileSafe(finalPath);
|
|
3245
|
-
worker.
|
|
3246
|
-
worker.fileSizes.push(fileSize);
|
|
3362
|
+
worker.recordFile(finalPath, fileSize);
|
|
3247
3363
|
const openResult = await DicomInstance.open(finalPath);
|
|
3248
3364
|
if (!openResult.ok) {
|
|
3249
3365
|
this.emit("error", {
|
|
3250
3366
|
error: openResult.error,
|
|
3251
3367
|
filePath: finalPath,
|
|
3252
|
-
associationId:
|
|
3253
|
-
associationDir:
|
|
3368
|
+
associationId: ctx.associationId,
|
|
3369
|
+
associationDir: ctx.associationDir,
|
|
3254
3370
|
callingAE: data.callingAE,
|
|
3255
3371
|
calledAE: data.calledAE,
|
|
3256
3372
|
source: data.source
|
|
@@ -3260,8 +3376,8 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3260
3376
|
this.emit("FILE_RECEIVED", {
|
|
3261
3377
|
filePath: finalPath,
|
|
3262
3378
|
fileSize,
|
|
3263
|
-
associationId:
|
|
3264
|
-
associationDir:
|
|
3379
|
+
associationId: ctx.associationId,
|
|
3380
|
+
associationDir: ctx.associationDir,
|
|
3265
3381
|
callingAE: data.callingAE,
|
|
3266
3382
|
calledAE: data.calledAE,
|
|
3267
3383
|
source: data.source,
|
|
@@ -3276,21 +3392,20 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3276
3392
|
}
|
|
3277
3393
|
/** Awaits ALL pending file operations, emits ASSOCIATION_COMPLETE, resets worker state. */
|
|
3278
3394
|
async finalizeAssociation(worker, data) {
|
|
3279
|
-
|
|
3280
|
-
await Promise.all(worker.pendingFiles);
|
|
3281
|
-
}
|
|
3395
|
+
await worker.drainPendingFiles();
|
|
3282
3396
|
this.emitAssociationComplete(worker, data);
|
|
3283
|
-
|
|
3397
|
+
worker.endAssociation();
|
|
3284
3398
|
void this.scaleDown();
|
|
3285
3399
|
}
|
|
3286
3400
|
/** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
|
|
3287
3401
|
emitAssociationComplete(worker, data) {
|
|
3288
|
-
const
|
|
3289
|
-
const
|
|
3402
|
+
const ctx = worker.context;
|
|
3403
|
+
const assocId = ctx?.associationId ?? data.associationId;
|
|
3404
|
+
const assocDir = ctx?.associationDir ?? "";
|
|
3405
|
+
const startAt = ctx?.startAt ?? Date.now();
|
|
3290
3406
|
const files = [...worker.files];
|
|
3291
3407
|
const output = [...worker.outputLines];
|
|
3292
3408
|
const endAt = Date.now();
|
|
3293
|
-
const startAt = worker.startAt ?? endAt;
|
|
3294
3409
|
const totalBytes = sumArray(worker.fileSizes);
|
|
3295
3410
|
const elapsedMs = endAt - startAt;
|
|
3296
3411
|
const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
|
|
@@ -3310,24 +3425,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3310
3425
|
output
|
|
3311
3426
|
});
|
|
3312
3427
|
}
|
|
3313
|
-
/** Resets worker state to idle after an association completes. */
|
|
3314
|
-
resetWorker(worker) {
|
|
3315
|
-
worker.state = "idle";
|
|
3316
|
-
worker.associationId = void 0;
|
|
3317
|
-
worker.associationDir = void 0;
|
|
3318
|
-
worker.files = [];
|
|
3319
|
-
worker.fileSizes = [];
|
|
3320
|
-
worker.outputLines = [];
|
|
3321
|
-
worker.pendingFiles = [];
|
|
3322
|
-
worker.startAt = void 0;
|
|
3323
|
-
worker.remoteSocket = void 0;
|
|
3324
|
-
worker.workerSocket = void 0;
|
|
3325
|
-
}
|
|
3326
3428
|
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
3327
3429
|
wireAssociationReceived(worker) {
|
|
3328
3430
|
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
3329
3431
|
this.emit("ASSOCIATION_RECEIVED", {
|
|
3330
|
-
associationId: worker.associationId ?? "",
|
|
3432
|
+
associationId: worker.context?.associationId ?? "",
|
|
3331
3433
|
callingAE: data.callingAE,
|
|
3332
3434
|
calledAE: data.calledAE,
|
|
3333
3435
|
source: data.source
|
|
@@ -3338,7 +3440,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3338
3440
|
wireCStoreRequest(worker) {
|
|
3339
3441
|
worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
|
|
3340
3442
|
this.emit("C_STORE_REQUEST", {
|
|
3341
|
-
associationId: worker.associationId ?? "",
|
|
3443
|
+
associationId: worker.context?.associationId ?? "",
|
|
3342
3444
|
raw: data.raw
|
|
3343
3445
|
});
|
|
3344
3446
|
});
|
|
@@ -3347,7 +3449,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3347
3449
|
wireEchoRequest(worker) {
|
|
3348
3450
|
worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
|
|
3349
3451
|
this.emit("ECHO_REQUEST", {
|
|
3350
|
-
associationId: worker.associationId ?? ""
|
|
3452
|
+
associationId: worker.context?.associationId ?? ""
|
|
3351
3453
|
});
|
|
3352
3454
|
});
|
|
3353
3455
|
}
|
|
@@ -3362,8 +3464,8 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3362
3464
|
/** Captures worker output lines during busy associations. */
|
|
3363
3465
|
wireOutputCapture(worker) {
|
|
3364
3466
|
worker.dcmrecv.on("line", ({ text }) => {
|
|
3365
|
-
if (worker.state === "busy"
|
|
3366
|
-
worker.
|
|
3467
|
+
if (worker.state === "busy") {
|
|
3468
|
+
worker.captureOutput(text);
|
|
3367
3469
|
}
|
|
3368
3470
|
});
|
|
3369
3471
|
}
|
|
@@ -3382,56 +3484,6 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
|
|
|
3382
3484
|
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
3383
3485
|
}
|
|
3384
3486
|
};
|
|
3385
|
-
async function ensureDirectory(dirPath) {
|
|
3386
|
-
try {
|
|
3387
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
3388
|
-
return ok(void 0);
|
|
3389
|
-
} catch (e) {
|
|
3390
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3391
|
-
return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
|
|
3392
|
-
}
|
|
3393
|
-
}
|
|
3394
|
-
async function moveFile(src, dest) {
|
|
3395
|
-
try {
|
|
3396
|
-
await fs.rename(src, dest);
|
|
3397
|
-
return ok(void 0);
|
|
3398
|
-
} catch {
|
|
3399
|
-
try {
|
|
3400
|
-
await fs.copyFile(src, dest);
|
|
3401
|
-
await fs.unlink(src);
|
|
3402
|
-
return ok(void 0);
|
|
3403
|
-
} catch (e) {
|
|
3404
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3405
|
-
return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
async function statFileSafe(filePath) {
|
|
3410
|
-
try {
|
|
3411
|
-
const stat3 = await fs.stat(filePath);
|
|
3412
|
-
return stat3.size;
|
|
3413
|
-
} catch {
|
|
3414
|
-
return 0;
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
function sumArray(arr) {
|
|
3418
|
-
let total = 0;
|
|
3419
|
-
for (let i = 0; i < arr.length; i++) {
|
|
3420
|
-
total += arr[i] ?? 0;
|
|
3421
|
-
}
|
|
3422
|
-
return total;
|
|
3423
|
-
}
|
|
3424
|
-
async function removeDirSafe(dirPath) {
|
|
3425
|
-
try {
|
|
3426
|
-
await fs.rm(dirPath, { recursive: true, force: true });
|
|
3427
|
-
} catch {
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
function delay(ms) {
|
|
3431
|
-
return new Promise((resolve) => {
|
|
3432
|
-
setTimeout(resolve, ms);
|
|
3433
|
-
});
|
|
3434
|
-
}
|
|
3435
3487
|
|
|
3436
3488
|
// src/events/dcmprscp.ts
|
|
3437
3489
|
var DcmprscpEvent = {
|