@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.cjs
CHANGED
|
@@ -6,10 +6,11 @@ var child_process = require('child_process');
|
|
|
6
6
|
var stderrLib = require('stderr-lib');
|
|
7
7
|
var kill = require('tree-kill');
|
|
8
8
|
var path = require('path');
|
|
9
|
-
var fs
|
|
9
|
+
var fs = require('fs');
|
|
10
10
|
var net = require('net');
|
|
11
|
-
var fs = require('fs/promises');
|
|
12
11
|
var os = require('os');
|
|
12
|
+
var promises = require('timers/promises');
|
|
13
|
+
var promises$1 = require('fs/promises');
|
|
13
14
|
var fastXmlParser = require('fast-xml-parser');
|
|
14
15
|
|
|
15
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -35,7 +36,6 @@ function _interopNamespace(e) {
|
|
|
35
36
|
var kill__default = /*#__PURE__*/_interopDefault(kill);
|
|
36
37
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
37
38
|
var net__namespace = /*#__PURE__*/_interopNamespace(net);
|
|
38
|
-
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
39
39
|
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
40
40
|
|
|
41
41
|
var __defProp = Object.defineProperty;
|
|
@@ -429,7 +429,7 @@ function binaryName(name) {
|
|
|
429
429
|
}
|
|
430
430
|
function hasRequiredBinaries(dir) {
|
|
431
431
|
for (const bin of REQUIRED_BINARIES) {
|
|
432
|
-
if (!fs
|
|
432
|
+
if (!fs.existsSync(path.join(dir, binaryName(bin)))) {
|
|
433
433
|
return false;
|
|
434
434
|
}
|
|
435
435
|
}
|
|
@@ -1274,6 +1274,44 @@ var StoreSCP = class _StoreSCP extends DcmtkProcess {
|
|
|
1274
1274
|
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
1275
1275
|
}
|
|
1276
1276
|
};
|
|
1277
|
+
async function ensureDirectory(dirPath) {
|
|
1278
|
+
try {
|
|
1279
|
+
await promises$1.mkdir(dirPath, { recursive: true });
|
|
1280
|
+
return ok(void 0);
|
|
1281
|
+
} catch (e) {
|
|
1282
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1283
|
+
return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
async function moveFile(src, dest) {
|
|
1287
|
+
try {
|
|
1288
|
+
await promises$1.rename(src, dest);
|
|
1289
|
+
return ok(void 0);
|
|
1290
|
+
} catch {
|
|
1291
|
+
try {
|
|
1292
|
+
await promises$1.copyFile(src, dest);
|
|
1293
|
+
await promises$1.unlink(src);
|
|
1294
|
+
return ok(void 0);
|
|
1295
|
+
} catch (e) {
|
|
1296
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1297
|
+
return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async function statFileSafe(filePath) {
|
|
1302
|
+
try {
|
|
1303
|
+
const s = await promises$1.stat(filePath);
|
|
1304
|
+
return s.size;
|
|
1305
|
+
} catch {
|
|
1306
|
+
return 0;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async function removeDirSafe(dirPath) {
|
|
1310
|
+
try {
|
|
1311
|
+
await promises$1.rm(dirPath, { recursive: true, force: true });
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1277
1315
|
function createDicomTag(input) {
|
|
1278
1316
|
if (!DICOM_TAG_PATTERN.test(input)) {
|
|
1279
1317
|
return err(new Error(`Invalid DICOM tag: "${input}". Expected format (XXXX,XXXX) where X is a hex digit`));
|
|
@@ -1337,227 +1375,6 @@ function tagPathToSegments(path2) {
|
|
|
1337
1375
|
return segments;
|
|
1338
1376
|
}
|
|
1339
1377
|
|
|
1340
|
-
// src/dicom/ChangeSet.ts
|
|
1341
|
-
var ERASE_PRIVATE_SENTINEL = "__ERASE_PRIVATE__";
|
|
1342
|
-
function isControlChar(code) {
|
|
1343
|
-
if (code <= 9) return true;
|
|
1344
|
-
if (code === 11 || code === 12) return true;
|
|
1345
|
-
if (code >= 14 && code <= 31) return true;
|
|
1346
|
-
return code === 127;
|
|
1347
|
-
}
|
|
1348
|
-
function sanitizeValue(value) {
|
|
1349
|
-
let result = "";
|
|
1350
|
-
for (let i = 0; i < value.length; i++) {
|
|
1351
|
-
const code = value.charCodeAt(i);
|
|
1352
|
-
if (!isControlChar(code)) {
|
|
1353
|
-
result += value[i];
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
return result;
|
|
1357
|
-
}
|
|
1358
|
-
function buildMergedModifications(base, other, erasures) {
|
|
1359
|
-
const merged = new Map(base);
|
|
1360
|
-
for (const [key, value] of other) {
|
|
1361
|
-
merged.set(key, value);
|
|
1362
|
-
}
|
|
1363
|
-
for (const key of erasures) {
|
|
1364
|
-
merged.delete(key);
|
|
1365
|
-
}
|
|
1366
|
-
return merged;
|
|
1367
|
-
}
|
|
1368
|
-
function applyBatchEntries(initial, entries) {
|
|
1369
|
-
const keys = Object.keys(entries);
|
|
1370
|
-
let cs = initial;
|
|
1371
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1372
|
-
const key = keys[i];
|
|
1373
|
-
if (key === void 0) continue;
|
|
1374
|
-
const value = entries[key];
|
|
1375
|
-
if (value === void 0) continue;
|
|
1376
|
-
cs = cs.setTag(key, value);
|
|
1377
|
-
}
|
|
1378
|
-
return cs;
|
|
1379
|
-
}
|
|
1380
|
-
var ChangeSet = class _ChangeSet {
|
|
1381
|
-
constructor(mods, erasures) {
|
|
1382
|
-
__publicField(this, "mods");
|
|
1383
|
-
__publicField(this, "erased");
|
|
1384
|
-
this.mods = mods;
|
|
1385
|
-
this.erased = erasures;
|
|
1386
|
-
}
|
|
1387
|
-
/** Creates an empty ChangeSet with no modifications or erasures. */
|
|
1388
|
-
static empty() {
|
|
1389
|
-
return new _ChangeSet(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* Sets a tag value, returning a new ChangeSet.
|
|
1393
|
-
*
|
|
1394
|
-
* Control characters (except LF/CR) are stripped from the value.
|
|
1395
|
-
* If the tag was previously erased, it is removed from the erasure set.
|
|
1396
|
-
*
|
|
1397
|
-
* @param path - The DICOM tag path to set (e.g. `'(0010,0010)'`)
|
|
1398
|
-
* @param value - The new value for the tag
|
|
1399
|
-
* @returns A new ChangeSet with the modification applied
|
|
1400
|
-
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1401
|
-
*/
|
|
1402
|
-
setTag(path2, value) {
|
|
1403
|
-
const totalOps = this.mods.size + this.erased.size;
|
|
1404
|
-
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1405
|
-
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1406
|
-
}
|
|
1407
|
-
tagPathToSegments(path2);
|
|
1408
|
-
const sanitized = sanitizeValue(value);
|
|
1409
|
-
const newMods = new Map(this.mods);
|
|
1410
|
-
newMods.set(path2, sanitized);
|
|
1411
|
-
const newErasures = new Set(this.erased);
|
|
1412
|
-
newErasures.delete(path2);
|
|
1413
|
-
return new _ChangeSet(newMods, newErasures);
|
|
1414
|
-
}
|
|
1415
|
-
/**
|
|
1416
|
-
* Marks a tag for erasure, returning a new ChangeSet.
|
|
1417
|
-
*
|
|
1418
|
-
* If the tag was previously set, the modification is removed.
|
|
1419
|
-
*
|
|
1420
|
-
* @param path - The DICOM tag path to erase (e.g. `'(0010,0010)'`)
|
|
1421
|
-
* @returns A new ChangeSet with the erasure applied
|
|
1422
|
-
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1423
|
-
*/
|
|
1424
|
-
eraseTag(path2) {
|
|
1425
|
-
const totalOps = this.mods.size + this.erased.size;
|
|
1426
|
-
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1427
|
-
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1428
|
-
}
|
|
1429
|
-
tagPathToSegments(path2);
|
|
1430
|
-
const newMods = new Map(this.mods);
|
|
1431
|
-
newMods.delete(path2);
|
|
1432
|
-
const newErasures = new Set(this.erased);
|
|
1433
|
-
newErasures.add(path2);
|
|
1434
|
-
return new _ChangeSet(newMods, newErasures);
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Marks all private tags for erasure, returning a new ChangeSet.
|
|
1438
|
-
*
|
|
1439
|
-
* @returns A new ChangeSet with the erase-private flag set
|
|
1440
|
-
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1441
|
-
*/
|
|
1442
|
-
erasePrivateTags() {
|
|
1443
|
-
const totalOps = this.mods.size + this.erased.size;
|
|
1444
|
-
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1445
|
-
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1446
|
-
}
|
|
1447
|
-
const newErasures = new Set(this.erased);
|
|
1448
|
-
newErasures.add(ERASE_PRIVATE_SENTINEL);
|
|
1449
|
-
return new _ChangeSet(new Map(this.mods), newErasures);
|
|
1450
|
-
}
|
|
1451
|
-
// -----------------------------------------------------------------------
|
|
1452
|
-
// Convenience setters for common DICOM tags
|
|
1453
|
-
// -----------------------------------------------------------------------
|
|
1454
|
-
/** Sets Patient's Name (0010,0010). */
|
|
1455
|
-
setPatientName(value) {
|
|
1456
|
-
return this.setTag("(0010,0010)", value);
|
|
1457
|
-
}
|
|
1458
|
-
/** Sets Patient ID (0010,0020). */
|
|
1459
|
-
setPatientID(value) {
|
|
1460
|
-
return this.setTag("(0010,0020)", value);
|
|
1461
|
-
}
|
|
1462
|
-
/** Sets Study Date (0008,0020). */
|
|
1463
|
-
setStudyDate(value) {
|
|
1464
|
-
return this.setTag("(0008,0020)", value);
|
|
1465
|
-
}
|
|
1466
|
-
/** Sets Modality (0008,0060). */
|
|
1467
|
-
setModality(value) {
|
|
1468
|
-
return this.setTag("(0008,0060)", value);
|
|
1469
|
-
}
|
|
1470
|
-
/** Sets Accession Number (0008,0050). */
|
|
1471
|
-
setAccessionNumber(value) {
|
|
1472
|
-
return this.setTag("(0008,0050)", value);
|
|
1473
|
-
}
|
|
1474
|
-
/** Sets Study Description (0008,1030). */
|
|
1475
|
-
setStudyDescription(value) {
|
|
1476
|
-
return this.setTag("(0008,1030)", value);
|
|
1477
|
-
}
|
|
1478
|
-
/** Sets Series Description (0008,103E). */
|
|
1479
|
-
setSeriesDescription(value) {
|
|
1480
|
-
return this.setTag("(0008,103E)", value);
|
|
1481
|
-
}
|
|
1482
|
-
/** Sets Institution Name (0008,0080). */
|
|
1483
|
-
setInstitutionName(value) {
|
|
1484
|
-
return this.setTag("(0008,0080)", value);
|
|
1485
|
-
}
|
|
1486
|
-
/**
|
|
1487
|
-
* Sets multiple tags at once, returning a new ChangeSet.
|
|
1488
|
-
*
|
|
1489
|
-
* @param entries - A record of tag path → value pairs
|
|
1490
|
-
* @returns A new ChangeSet with all modifications applied
|
|
1491
|
-
*/
|
|
1492
|
-
setBatch(entries) {
|
|
1493
|
-
return applyBatchEntries(this, entries);
|
|
1494
|
-
}
|
|
1495
|
-
/** All pending tag modifications as a readonly map of path → value. */
|
|
1496
|
-
get modifications() {
|
|
1497
|
-
return this.mods;
|
|
1498
|
-
}
|
|
1499
|
-
/** All pending tag erasures as a readonly set of paths. */
|
|
1500
|
-
get erasures() {
|
|
1501
|
-
return this.erased;
|
|
1502
|
-
}
|
|
1503
|
-
/** Total number of operations (modifications + erasures) in this ChangeSet. */
|
|
1504
|
-
get operationCount() {
|
|
1505
|
-
return this.mods.size + this.erased.size;
|
|
1506
|
-
}
|
|
1507
|
-
/** Whether the ChangeSet has no modifications and no erasures. */
|
|
1508
|
-
get isEmpty() {
|
|
1509
|
-
return this.mods.size === 0 && this.erased.size === 0;
|
|
1510
|
-
}
|
|
1511
|
-
/** Whether the erase-all-private-tags flag is set. */
|
|
1512
|
-
get erasePrivate() {
|
|
1513
|
-
return this.erased.has(ERASE_PRIVATE_SENTINEL);
|
|
1514
|
-
}
|
|
1515
|
-
/**
|
|
1516
|
-
* Merges another ChangeSet into this one, returning a new ChangeSet.
|
|
1517
|
-
*
|
|
1518
|
-
* The `other` ChangeSet wins on conflicts: if the same tag is modified in both,
|
|
1519
|
-
* `other`'s value is used. Erasures from both sets are unioned. An erasure in
|
|
1520
|
-
* `other` removes a modification from `base`.
|
|
1521
|
-
*
|
|
1522
|
-
* @param other - The ChangeSet to merge in
|
|
1523
|
-
* @returns A new ChangeSet with merged modifications and erasures
|
|
1524
|
-
*/
|
|
1525
|
-
merge(other) {
|
|
1526
|
-
const mergedErasures = /* @__PURE__ */ new Set([...this.erased, ...other.erased]);
|
|
1527
|
-
const mergedMods = buildMergedModifications(this.mods, other.mods, mergedErasures);
|
|
1528
|
-
return new _ChangeSet(mergedMods, mergedErasures);
|
|
1529
|
-
}
|
|
1530
|
-
/**
|
|
1531
|
-
* Converts modifications to dcmodify-compatible TagModification array.
|
|
1532
|
-
*
|
|
1533
|
-
* @returns A readonly array of TagModification objects
|
|
1534
|
-
*/
|
|
1535
|
-
toModifications() {
|
|
1536
|
-
const result = [];
|
|
1537
|
-
for (const [tag, value] of this.mods) {
|
|
1538
|
-
result.push({ tag, value });
|
|
1539
|
-
}
|
|
1540
|
-
return result;
|
|
1541
|
-
}
|
|
1542
|
-
/**
|
|
1543
|
-
* Converts erasures to dcmodify-compatible argument strings.
|
|
1544
|
-
*
|
|
1545
|
-
* The erase-private sentinel is excluded — use {@link erasePrivate} to check
|
|
1546
|
-
* whether `-ep` should be passed.
|
|
1547
|
-
*
|
|
1548
|
-
* @returns A readonly array of tag path strings for `-e` arguments
|
|
1549
|
-
*/
|
|
1550
|
-
toErasureArgs() {
|
|
1551
|
-
const result = [];
|
|
1552
|
-
for (const path2 of this.erased) {
|
|
1553
|
-
if (path2 !== ERASE_PRIVATE_SENTINEL) {
|
|
1554
|
-
result.push(path2);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
return result;
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
|
|
1561
1378
|
// src/dicom/walkTags.ts
|
|
1562
1379
|
var DEFAULT_MAX_DEPTH = 16;
|
|
1563
1380
|
function walkTags(data, options) {
|
|
@@ -1901,83 +1718,304 @@ var DicomDataset = class _DicomDataset {
|
|
|
1901
1718
|
} catch (e) {
|
|
1902
1719
|
return err(stderrLib.stderr(e));
|
|
1903
1720
|
}
|
|
1904
|
-
return traversePath(this.data, segments);
|
|
1721
|
+
return traversePath(this.data, segments);
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Finds all values matching a path with wildcard `[*]` indices.
|
|
1725
|
+
*
|
|
1726
|
+
* Traverses all items in wildcard sequence positions using an iterative BFS queue.
|
|
1727
|
+
* The traversal is bounded to {@link MAX_TRAVERSAL_DEPTH} * 100 iterations (5 000).
|
|
1728
|
+
* For extremely large datasets this may truncate results silently; callers
|
|
1729
|
+
* needing completeness guarantees should verify dataset size independently.
|
|
1730
|
+
*
|
|
1731
|
+
* @param path - A branded DicomTagPath, e.g. `(0040,A730)[*].(0040,A160)`
|
|
1732
|
+
* @returns A readonly array of all matching values (may be empty)
|
|
1733
|
+
*/
|
|
1734
|
+
findValues(path2) {
|
|
1735
|
+
let segments;
|
|
1736
|
+
try {
|
|
1737
|
+
segments = tagPathToSegments(path2);
|
|
1738
|
+
} catch {
|
|
1739
|
+
return [];
|
|
1740
|
+
}
|
|
1741
|
+
const result = collectWildcard(this.data, segments);
|
|
1742
|
+
return result.values;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Walks all tags in this dataset, optionally filtering by VR and recursing into sequences.
|
|
1746
|
+
*
|
|
1747
|
+
* @param options - Optional VR filter and max depth
|
|
1748
|
+
* @returns A readonly array of all matching tag entries
|
|
1749
|
+
*/
|
|
1750
|
+
walkTags(options) {
|
|
1751
|
+
return walkTags(this.data, options);
|
|
1752
|
+
}
|
|
1753
|
+
// -----------------------------------------------------------------------
|
|
1754
|
+
// Convenience readonly getters
|
|
1755
|
+
// -----------------------------------------------------------------------
|
|
1756
|
+
/** Accession Number (0008,0050). */
|
|
1757
|
+
get accession() {
|
|
1758
|
+
return this.getString(TAGS.AccessionNumber);
|
|
1759
|
+
}
|
|
1760
|
+
/** Patient's Name (0010,0010). */
|
|
1761
|
+
get patientName() {
|
|
1762
|
+
return this.getString(TAGS.PatientName);
|
|
1763
|
+
}
|
|
1764
|
+
/** Patient ID (0010,0020). */
|
|
1765
|
+
get patientID() {
|
|
1766
|
+
return this.getString(TAGS.PatientID);
|
|
1767
|
+
}
|
|
1768
|
+
/** Study Date (0008,0020). */
|
|
1769
|
+
get studyDate() {
|
|
1770
|
+
return this.getString(TAGS.StudyDate);
|
|
1771
|
+
}
|
|
1772
|
+
/** Modality (0008,0060). */
|
|
1773
|
+
get modality() {
|
|
1774
|
+
return this.getString(TAGS.Modality);
|
|
1775
|
+
}
|
|
1776
|
+
/** SOP Class UID (0008,0016) as a branded SOPClassUID, or undefined if missing/invalid. */
|
|
1777
|
+
get sopClassUID() {
|
|
1778
|
+
const uid = this.getString(TAGS.SOPClassUID);
|
|
1779
|
+
if (uid.length === 0) return void 0;
|
|
1780
|
+
const result = createSOPClassUID(uid);
|
|
1781
|
+
return result.ok ? result.value : void 0;
|
|
1782
|
+
}
|
|
1783
|
+
/** Study Instance UID (0020,000D). */
|
|
1784
|
+
get studyInstanceUID() {
|
|
1785
|
+
return this.getString(TAGS.StudyInstanceUID);
|
|
1786
|
+
}
|
|
1787
|
+
/** Series Instance UID (0020,000E). */
|
|
1788
|
+
get seriesInstanceUID() {
|
|
1789
|
+
return this.getString(TAGS.SeriesInstanceUID);
|
|
1790
|
+
}
|
|
1791
|
+
/** SOP Instance UID (0008,0018). */
|
|
1792
|
+
get sopInstanceUID() {
|
|
1793
|
+
return this.getString(TAGS.SOPInstanceUID);
|
|
1794
|
+
}
|
|
1795
|
+
/** Transfer Syntax UID (0002,0010). */
|
|
1796
|
+
get transferSyntaxUID() {
|
|
1797
|
+
return this.getString(TAGS.TransferSyntaxUID);
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
// src/dicom/ChangeSet.ts
|
|
1802
|
+
var ERASE_PRIVATE_SENTINEL = "__ERASE_PRIVATE__";
|
|
1803
|
+
function isControlChar(code) {
|
|
1804
|
+
if (code <= 9) return true;
|
|
1805
|
+
if (code === 11 || code === 12) return true;
|
|
1806
|
+
if (code >= 14 && code <= 31) return true;
|
|
1807
|
+
return code === 127;
|
|
1808
|
+
}
|
|
1809
|
+
function sanitizeValue(value) {
|
|
1810
|
+
let result = "";
|
|
1811
|
+
for (let i = 0; i < value.length; i++) {
|
|
1812
|
+
const code = value.charCodeAt(i);
|
|
1813
|
+
if (!isControlChar(code)) {
|
|
1814
|
+
result += value[i];
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return result;
|
|
1818
|
+
}
|
|
1819
|
+
function buildMergedModifications(base, other, erasures) {
|
|
1820
|
+
const merged = new Map(base);
|
|
1821
|
+
for (const [key, value] of other) {
|
|
1822
|
+
merged.set(key, value);
|
|
1823
|
+
}
|
|
1824
|
+
for (const key of erasures) {
|
|
1825
|
+
merged.delete(key);
|
|
1826
|
+
}
|
|
1827
|
+
return merged;
|
|
1828
|
+
}
|
|
1829
|
+
function applyBatchEntries(initial, entries) {
|
|
1830
|
+
const keys = Object.keys(entries);
|
|
1831
|
+
let cs = initial;
|
|
1832
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1833
|
+
const key = keys[i];
|
|
1834
|
+
if (key === void 0) continue;
|
|
1835
|
+
const value = entries[key];
|
|
1836
|
+
if (value === void 0) continue;
|
|
1837
|
+
cs = cs.setTag(key, value);
|
|
1838
|
+
}
|
|
1839
|
+
return cs;
|
|
1840
|
+
}
|
|
1841
|
+
var ChangeSet = class _ChangeSet {
|
|
1842
|
+
constructor(mods, erasures) {
|
|
1843
|
+
__publicField(this, "mods");
|
|
1844
|
+
__publicField(this, "erased");
|
|
1845
|
+
this.mods = mods;
|
|
1846
|
+
this.erased = erasures;
|
|
1847
|
+
}
|
|
1848
|
+
/** Creates an empty ChangeSet with no modifications or erasures. */
|
|
1849
|
+
static empty() {
|
|
1850
|
+
return new _ChangeSet(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Set());
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Sets a tag value, returning a new ChangeSet.
|
|
1854
|
+
*
|
|
1855
|
+
* Control characters (except LF/CR) are stripped from the value.
|
|
1856
|
+
* If the tag was previously erased, it is removed from the erasure set.
|
|
1857
|
+
*
|
|
1858
|
+
* @param path - The DICOM tag path to set (e.g. `'(0010,0010)'`)
|
|
1859
|
+
* @param value - The new value for the tag
|
|
1860
|
+
* @returns A new ChangeSet with the modification applied
|
|
1861
|
+
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1862
|
+
*/
|
|
1863
|
+
setTag(path2, value) {
|
|
1864
|
+
const totalOps = this.mods.size + this.erased.size;
|
|
1865
|
+
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1866
|
+
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1867
|
+
}
|
|
1868
|
+
tagPathToSegments(path2);
|
|
1869
|
+
const sanitized = sanitizeValue(value);
|
|
1870
|
+
const newMods = new Map(this.mods);
|
|
1871
|
+
newMods.set(path2, sanitized);
|
|
1872
|
+
const newErasures = new Set(this.erased);
|
|
1873
|
+
newErasures.delete(path2);
|
|
1874
|
+
return new _ChangeSet(newMods, newErasures);
|
|
1905
1875
|
}
|
|
1906
1876
|
/**
|
|
1907
|
-
*
|
|
1877
|
+
* Marks a tag for erasure, returning a new ChangeSet.
|
|
1908
1878
|
*
|
|
1909
|
-
*
|
|
1910
|
-
* The traversal is bounded to {@link MAX_TRAVERSAL_DEPTH} * 100 iterations (5 000).
|
|
1911
|
-
* For extremely large datasets this may truncate results silently; callers
|
|
1912
|
-
* needing completeness guarantees should verify dataset size independently.
|
|
1879
|
+
* If the tag was previously set, the modification is removed.
|
|
1913
1880
|
*
|
|
1914
|
-
* @param path -
|
|
1915
|
-
* @returns A
|
|
1881
|
+
* @param path - The DICOM tag path to erase (e.g. `'(0010,0010)'`)
|
|
1882
|
+
* @returns A new ChangeSet with the erasure applied
|
|
1883
|
+
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1916
1884
|
*/
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
} catch {
|
|
1922
|
-
return [];
|
|
1885
|
+
eraseTag(path2) {
|
|
1886
|
+
const totalOps = this.mods.size + this.erased.size;
|
|
1887
|
+
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1888
|
+
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1923
1889
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1890
|
+
tagPathToSegments(path2);
|
|
1891
|
+
const newMods = new Map(this.mods);
|
|
1892
|
+
newMods.delete(path2);
|
|
1893
|
+
const newErasures = new Set(this.erased);
|
|
1894
|
+
newErasures.add(path2);
|
|
1895
|
+
return new _ChangeSet(newMods, newErasures);
|
|
1926
1896
|
}
|
|
1927
1897
|
/**
|
|
1928
|
-
*
|
|
1898
|
+
* Marks all private tags for erasure, returning a new ChangeSet.
|
|
1929
1899
|
*
|
|
1930
|
-
* @
|
|
1931
|
-
* @
|
|
1900
|
+
* @returns A new ChangeSet with the erase-private flag set
|
|
1901
|
+
* @throws Error if operation count would exceed MAX_CHANGESET_OPERATIONS
|
|
1932
1902
|
*/
|
|
1933
|
-
|
|
1934
|
-
|
|
1903
|
+
erasePrivateTags() {
|
|
1904
|
+
const totalOps = this.mods.size + this.erased.size;
|
|
1905
|
+
if (totalOps >= MAX_CHANGESET_OPERATIONS) {
|
|
1906
|
+
throw new Error(`ChangeSet operation limit (${MAX_CHANGESET_OPERATIONS}) exceeded`);
|
|
1907
|
+
}
|
|
1908
|
+
const newErasures = new Set(this.erased);
|
|
1909
|
+
newErasures.add(ERASE_PRIVATE_SENTINEL);
|
|
1910
|
+
return new _ChangeSet(new Map(this.mods), newErasures);
|
|
1935
1911
|
}
|
|
1936
1912
|
// -----------------------------------------------------------------------
|
|
1937
|
-
// Convenience
|
|
1913
|
+
// Convenience setters for common DICOM tags
|
|
1938
1914
|
// -----------------------------------------------------------------------
|
|
1939
|
-
/**
|
|
1940
|
-
|
|
1941
|
-
return this.
|
|
1915
|
+
/** Sets Patient's Name (0010,0010). */
|
|
1916
|
+
setPatientName(value) {
|
|
1917
|
+
return this.setTag("(0010,0010)", value);
|
|
1942
1918
|
}
|
|
1943
|
-
/** Patient
|
|
1944
|
-
|
|
1945
|
-
return this.
|
|
1919
|
+
/** Sets Patient ID (0010,0020). */
|
|
1920
|
+
setPatientID(value) {
|
|
1921
|
+
return this.setTag("(0010,0020)", value);
|
|
1946
1922
|
}
|
|
1947
|
-
/**
|
|
1948
|
-
|
|
1949
|
-
return this.
|
|
1923
|
+
/** Sets Study Date (0008,0020). */
|
|
1924
|
+
setStudyDate(value) {
|
|
1925
|
+
return this.setTag("(0008,0020)", value);
|
|
1950
1926
|
}
|
|
1951
|
-
/**
|
|
1952
|
-
|
|
1953
|
-
return this.
|
|
1927
|
+
/** Sets Modality (0008,0060). */
|
|
1928
|
+
setModality(value) {
|
|
1929
|
+
return this.setTag("(0008,0060)", value);
|
|
1954
1930
|
}
|
|
1955
|
-
/**
|
|
1956
|
-
|
|
1957
|
-
return this.
|
|
1931
|
+
/** Sets Accession Number (0008,0050). */
|
|
1932
|
+
setAccessionNumber(value) {
|
|
1933
|
+
return this.setTag("(0008,0050)", value);
|
|
1958
1934
|
}
|
|
1959
|
-
/**
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
if (uid.length === 0) return void 0;
|
|
1963
|
-
const result = createSOPClassUID(uid);
|
|
1964
|
-
return result.ok ? result.value : void 0;
|
|
1935
|
+
/** Sets Study Description (0008,1030). */
|
|
1936
|
+
setStudyDescription(value) {
|
|
1937
|
+
return this.setTag("(0008,1030)", value);
|
|
1965
1938
|
}
|
|
1966
|
-
/**
|
|
1967
|
-
|
|
1968
|
-
return this.
|
|
1939
|
+
/** Sets Series Description (0008,103E). */
|
|
1940
|
+
setSeriesDescription(value) {
|
|
1941
|
+
return this.setTag("(0008,103E)", value);
|
|
1969
1942
|
}
|
|
1970
|
-
/**
|
|
1971
|
-
|
|
1972
|
-
return this.
|
|
1943
|
+
/** Sets Institution Name (0008,0080). */
|
|
1944
|
+
setInstitutionName(value) {
|
|
1945
|
+
return this.setTag("(0008,0080)", value);
|
|
1973
1946
|
}
|
|
1974
|
-
/**
|
|
1975
|
-
|
|
1976
|
-
|
|
1947
|
+
/**
|
|
1948
|
+
* Sets multiple tags at once, returning a new ChangeSet.
|
|
1949
|
+
*
|
|
1950
|
+
* @param entries - A record of tag path → value pairs
|
|
1951
|
+
* @returns A new ChangeSet with all modifications applied
|
|
1952
|
+
*/
|
|
1953
|
+
setBatch(entries) {
|
|
1954
|
+
return applyBatchEntries(this, entries);
|
|
1977
1955
|
}
|
|
1978
|
-
/**
|
|
1979
|
-
get
|
|
1980
|
-
return this.
|
|
1956
|
+
/** All pending tag modifications as a readonly map of path → value. */
|
|
1957
|
+
get modifications() {
|
|
1958
|
+
return this.mods;
|
|
1959
|
+
}
|
|
1960
|
+
/** All pending tag erasures as a readonly set of paths. */
|
|
1961
|
+
get erasures() {
|
|
1962
|
+
return this.erased;
|
|
1963
|
+
}
|
|
1964
|
+
/** Total number of operations (modifications + erasures) in this ChangeSet. */
|
|
1965
|
+
get operationCount() {
|
|
1966
|
+
return this.mods.size + this.erased.size;
|
|
1967
|
+
}
|
|
1968
|
+
/** Whether the ChangeSet has no modifications and no erasures. */
|
|
1969
|
+
get isEmpty() {
|
|
1970
|
+
return this.mods.size === 0 && this.erased.size === 0;
|
|
1971
|
+
}
|
|
1972
|
+
/** Whether the erase-all-private-tags flag is set. */
|
|
1973
|
+
get erasePrivate() {
|
|
1974
|
+
return this.erased.has(ERASE_PRIVATE_SENTINEL);
|
|
1975
|
+
}
|
|
1976
|
+
/**
|
|
1977
|
+
* Merges another ChangeSet into this one, returning a new ChangeSet.
|
|
1978
|
+
*
|
|
1979
|
+
* The `other` ChangeSet wins on conflicts: if the same tag is modified in both,
|
|
1980
|
+
* `other`'s value is used. Erasures from both sets are unioned. An erasure in
|
|
1981
|
+
* `other` removes a modification from `base`.
|
|
1982
|
+
*
|
|
1983
|
+
* @param other - The ChangeSet to merge in
|
|
1984
|
+
* @returns A new ChangeSet with merged modifications and erasures
|
|
1985
|
+
*/
|
|
1986
|
+
merge(other) {
|
|
1987
|
+
const mergedErasures = /* @__PURE__ */ new Set([...this.erased, ...other.erased]);
|
|
1988
|
+
const mergedMods = buildMergedModifications(this.mods, other.mods, mergedErasures);
|
|
1989
|
+
return new _ChangeSet(mergedMods, mergedErasures);
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Converts modifications to dcmodify-compatible TagModification array.
|
|
1993
|
+
*
|
|
1994
|
+
* @returns A readonly array of TagModification objects
|
|
1995
|
+
*/
|
|
1996
|
+
toModifications() {
|
|
1997
|
+
const result = [];
|
|
1998
|
+
for (const [tag, value] of this.mods) {
|
|
1999
|
+
result.push({ tag, value });
|
|
2000
|
+
}
|
|
2001
|
+
return result;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Converts erasures to dcmodify-compatible argument strings.
|
|
2005
|
+
*
|
|
2006
|
+
* The erase-private sentinel is excluded — use {@link erasePrivate} to check
|
|
2007
|
+
* whether `-ep` should be passed.
|
|
2008
|
+
*
|
|
2009
|
+
* @returns A readonly array of tag path strings for `-e` arguments
|
|
2010
|
+
*/
|
|
2011
|
+
toErasureArgs() {
|
|
2012
|
+
const result = [];
|
|
2013
|
+
for (const path2 of this.erased) {
|
|
2014
|
+
if (path2 !== ERASE_PRIVATE_SENTINEL) {
|
|
2015
|
+
result.push(path2);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return result;
|
|
1981
2019
|
}
|
|
1982
2020
|
};
|
|
1983
2021
|
function killTree(pid) {
|
|
@@ -1994,13 +2032,14 @@ async function execCommand(binary, args, options) {
|
|
|
1994
2032
|
windowsHide: true,
|
|
1995
2033
|
signal: options?.signal
|
|
1996
2034
|
});
|
|
1997
|
-
wireSpawnListeners(child, timeoutMs, resolve);
|
|
2035
|
+
wireSpawnListeners(binary, child, timeoutMs, resolve);
|
|
1998
2036
|
});
|
|
1999
2037
|
}
|
|
2000
|
-
function wireSpawnListeners(child, timeoutMs, resolve) {
|
|
2038
|
+
function wireSpawnListeners(binary, child, timeoutMs, resolve) {
|
|
2001
2039
|
let stdout = "";
|
|
2002
2040
|
let stderr6 = "";
|
|
2003
2041
|
let settled = false;
|
|
2042
|
+
const name = binary.split("/").pop() ?? binary;
|
|
2004
2043
|
const settle = (result) => {
|
|
2005
2044
|
if (settled) return;
|
|
2006
2045
|
settled = true;
|
|
@@ -2009,25 +2048,25 @@ function wireSpawnListeners(child, timeoutMs, resolve) {
|
|
|
2009
2048
|
};
|
|
2010
2049
|
const timer = setTimeout(() => {
|
|
2011
2050
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
2012
|
-
settle(err(new Error(
|
|
2051
|
+
settle(err(new Error(`${name} timed out after ${timeoutMs}ms (pid: ${String(child.pid ?? "unknown")})`)));
|
|
2013
2052
|
}, timeoutMs);
|
|
2014
2053
|
child.stdout?.on("data", (chunk) => {
|
|
2015
2054
|
stdout += String(chunk);
|
|
2016
2055
|
if (stdout.length + stderr6.length > MAX_OUTPUT_BYTES) {
|
|
2017
2056
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
2018
|
-
settle(err(new Error(
|
|
2057
|
+
settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
|
|
2019
2058
|
}
|
|
2020
2059
|
});
|
|
2021
2060
|
child.stderr?.on("data", (chunk) => {
|
|
2022
2061
|
stderr6 += String(chunk);
|
|
2023
2062
|
if (stdout.length + stderr6.length > MAX_OUTPUT_BYTES) {
|
|
2024
2063
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
2025
|
-
settle(err(new Error(
|
|
2064
|
+
settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
|
|
2026
2065
|
}
|
|
2027
2066
|
});
|
|
2028
2067
|
child.on("error", (error) => {
|
|
2029
2068
|
if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
|
|
2030
|
-
settle(err(new Error(
|
|
2069
|
+
settle(err(new Error(`${name} error: ${error.message}`)));
|
|
2031
2070
|
});
|
|
2032
2071
|
child.on("close", (code) => {
|
|
2033
2072
|
settle(ok({ stdout, stderr: stderr6, exitCode: code ?? 1 }));
|
|
@@ -2042,7 +2081,7 @@ async function spawnCommand(binary, args, options) {
|
|
|
2042
2081
|
windowsHide: true,
|
|
2043
2082
|
signal: options?.signal
|
|
2044
2083
|
});
|
|
2045
|
-
wireSpawnListeners(child, timeoutMs, resolve);
|
|
2084
|
+
wireSpawnListeners(binary, child, timeoutMs, resolve);
|
|
2046
2085
|
});
|
|
2047
2086
|
}
|
|
2048
2087
|
var PN_REPS = ["Alphabetic", "Ideographic", "Phonetic"];
|
|
@@ -2314,12 +2353,12 @@ async function tryDirectPath(inputPath, timeoutMs, signal, verbosity) {
|
|
|
2314
2353
|
const execOpts = signal !== void 0 ? { timeoutMs, signal } : { timeoutMs };
|
|
2315
2354
|
return await execAndParse(jsonBinary.value, directArgs, inputPath, execOpts);
|
|
2316
2355
|
} finally {
|
|
2317
|
-
|
|
2356
|
+
promises$1.rm(bulkDir, { recursive: true, force: true }).catch(() => {
|
|
2318
2357
|
});
|
|
2319
2358
|
}
|
|
2320
2359
|
}
|
|
2321
2360
|
async function createBulkTempDir() {
|
|
2322
|
-
return
|
|
2361
|
+
return promises$1.mkdtemp(path.join(os.tmpdir(), "dcm2json-bulk-"));
|
|
2323
2362
|
}
|
|
2324
2363
|
async function execAndParse(binary, args, inputPath, execOpts) {
|
|
2325
2364
|
const result = await execCommand(binary, args, execOpts);
|
|
@@ -2441,19 +2480,19 @@ async function applyModifications(filePath, changeset, options) {
|
|
|
2441
2480
|
}
|
|
2442
2481
|
async function copyFileSafe(source, dest) {
|
|
2443
2482
|
return stderrLib.tryCatch(
|
|
2444
|
-
() =>
|
|
2483
|
+
() => promises$1.copyFile(source, dest),
|
|
2445
2484
|
(e) => new Error(`Failed to copy file: ${e.message}`)
|
|
2446
2485
|
);
|
|
2447
2486
|
}
|
|
2448
2487
|
async function statFileSize(path2) {
|
|
2449
2488
|
return stderrLib.tryCatch(
|
|
2450
|
-
async () => (await
|
|
2489
|
+
async () => (await promises$1.stat(path2)).size,
|
|
2451
2490
|
(e) => new Error(`Failed to stat file: ${e.message}`)
|
|
2452
2491
|
);
|
|
2453
2492
|
}
|
|
2454
2493
|
async function unlinkFile(path2) {
|
|
2455
2494
|
return stderrLib.tryCatch(
|
|
2456
|
-
() =>
|
|
2495
|
+
() => promises$1.unlink(path2),
|
|
2457
2496
|
(e) => new Error(`Failed to delete file: ${e.message}`)
|
|
2458
2497
|
);
|
|
2459
2498
|
}
|
|
@@ -2808,6 +2847,102 @@ var DicomReceiverOptionsSchema = zod.z.object({
|
|
|
2808
2847
|
}).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
|
|
2809
2848
|
message: "minPoolSize must be <= maxPoolSize"
|
|
2810
2849
|
});
|
|
2850
|
+
var Worker = class {
|
|
2851
|
+
constructor(dcmrecv, port, tempDir) {
|
|
2852
|
+
__publicField(this, "dcmrecv");
|
|
2853
|
+
__publicField(this, "port");
|
|
2854
|
+
__publicField(this, "tempDir");
|
|
2855
|
+
__publicField(this, "_state", "idle");
|
|
2856
|
+
__publicField(this, "_context");
|
|
2857
|
+
__publicField(this, "_pending", /* @__PURE__ */ new Set());
|
|
2858
|
+
__publicField(this, "_files", []);
|
|
2859
|
+
__publicField(this, "_fileSizes", []);
|
|
2860
|
+
__publicField(this, "_outputLines", []);
|
|
2861
|
+
__publicField(this, "_remoteSocket");
|
|
2862
|
+
__publicField(this, "_workerSocket");
|
|
2863
|
+
this.dcmrecv = dcmrecv;
|
|
2864
|
+
this.port = port;
|
|
2865
|
+
this.tempDir = tempDir;
|
|
2866
|
+
}
|
|
2867
|
+
/** Current worker state. */
|
|
2868
|
+
get state() {
|
|
2869
|
+
return this._state;
|
|
2870
|
+
}
|
|
2871
|
+
/** Active association context, or undefined when idle. */
|
|
2872
|
+
get context() {
|
|
2873
|
+
return this._context;
|
|
2874
|
+
}
|
|
2875
|
+
/** Files moved to the association directory during the current association. */
|
|
2876
|
+
get files() {
|
|
2877
|
+
return this._files;
|
|
2878
|
+
}
|
|
2879
|
+
/** Byte sizes parallel to files[], for transfer stats. */
|
|
2880
|
+
get fileSizes() {
|
|
2881
|
+
return this._fileSizes;
|
|
2882
|
+
}
|
|
2883
|
+
/** Captured output lines during the current association. */
|
|
2884
|
+
get outputLines() {
|
|
2885
|
+
return this._outputLines;
|
|
2886
|
+
}
|
|
2887
|
+
/** Marks the worker busy with a new association context. */
|
|
2888
|
+
beginAssociation(ctx) {
|
|
2889
|
+
this._state = "busy";
|
|
2890
|
+
this._context = ctx;
|
|
2891
|
+
this._files.length = 0;
|
|
2892
|
+
this._fileSizes.length = 0;
|
|
2893
|
+
this._outputLines.length = 0;
|
|
2894
|
+
this._pending.clear();
|
|
2895
|
+
}
|
|
2896
|
+
/** Returns the worker to idle and clears all association state. */
|
|
2897
|
+
endAssociation() {
|
|
2898
|
+
this._state = "idle";
|
|
2899
|
+
this._context = void 0;
|
|
2900
|
+
this._files.length = 0;
|
|
2901
|
+
this._fileSizes.length = 0;
|
|
2902
|
+
this._outputLines.length = 0;
|
|
2903
|
+
this._pending.clear();
|
|
2904
|
+
this._remoteSocket = void 0;
|
|
2905
|
+
this._workerSocket = void 0;
|
|
2906
|
+
}
|
|
2907
|
+
/** Tracks an in-flight file handling promise; auto-removes on completion. */
|
|
2908
|
+
trackFile(promise) {
|
|
2909
|
+
this._pending.add(promise);
|
|
2910
|
+
void promise.finally(() => {
|
|
2911
|
+
this._pending.delete(promise);
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
/** Records a successfully received file path and its size. */
|
|
2915
|
+
recordFile(filePath, size) {
|
|
2916
|
+
this._files.push(filePath);
|
|
2917
|
+
this._fileSizes.push(size);
|
|
2918
|
+
}
|
|
2919
|
+
/** Awaits all in-flight file handling promises. */
|
|
2920
|
+
async drainPendingFiles() {
|
|
2921
|
+
if (this._pending.size > 0) {
|
|
2922
|
+
await Promise.all(this._pending);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
/** Appends a line to the output buffer, respecting the cap. */
|
|
2926
|
+
captureOutput(text) {
|
|
2927
|
+
if (this._outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
|
|
2928
|
+
this._outputLines.push(text);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
/** Stores the remote and worker sockets for later cleanup. */
|
|
2932
|
+
setSockets(remote, worker) {
|
|
2933
|
+
this._remoteSocket = remote;
|
|
2934
|
+
this._workerSocket = worker;
|
|
2935
|
+
}
|
|
2936
|
+
/** Destroys both sockets if they exist and are not already destroyed. */
|
|
2937
|
+
destroySockets() {
|
|
2938
|
+
if (this._remoteSocket !== void 0 && !this._remoteSocket.destroyed) {
|
|
2939
|
+
this._remoteSocket.destroy();
|
|
2940
|
+
}
|
|
2941
|
+
if (this._workerSocket !== void 0 && !this._workerSocket.destroyed) {
|
|
2942
|
+
this._workerSocket.destroy();
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2811
2946
|
function allocatePort() {
|
|
2812
2947
|
return new Promise((resolve) => {
|
|
2813
2948
|
const server = net__namespace.createServer();
|
|
@@ -2825,6 +2960,13 @@ function allocatePort() {
|
|
|
2825
2960
|
});
|
|
2826
2961
|
});
|
|
2827
2962
|
}
|
|
2963
|
+
function sumArray(arr) {
|
|
2964
|
+
let total = 0;
|
|
2965
|
+
for (let i = 0; i < arr.length; i++) {
|
|
2966
|
+
total += arr[i] ?? 0;
|
|
2967
|
+
}
|
|
2968
|
+
return total;
|
|
2969
|
+
}
|
|
2828
2970
|
var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
2829
2971
|
constructor(options) {
|
|
2830
2972
|
super();
|
|
@@ -2839,8 +2981,6 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
2839
2981
|
__publicField(this, "stopping", false);
|
|
2840
2982
|
__publicField(this, "abortHandler");
|
|
2841
2983
|
this.setMaxListeners(20);
|
|
2842
|
-
this.on("error", () => {
|
|
2843
|
-
});
|
|
2844
2984
|
this.options = options;
|
|
2845
2985
|
this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
|
|
2846
2986
|
this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
|
|
@@ -3048,14 +3188,12 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3048
3188
|
this.emit("error", { error: mkdirResult.error });
|
|
3049
3189
|
return;
|
|
3050
3190
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
worker.
|
|
3057
|
-
worker.pendingFiles = [];
|
|
3058
|
-
worker.startAt = Date.now();
|
|
3191
|
+
const ctx = {
|
|
3192
|
+
associationId,
|
|
3193
|
+
associationDir,
|
|
3194
|
+
startAt: Date.now()
|
|
3195
|
+
};
|
|
3196
|
+
worker.beginAssociation(ctx);
|
|
3059
3197
|
this.pipeConnection(worker, remoteSocket);
|
|
3060
3198
|
void this.replenishPool();
|
|
3061
3199
|
}
|
|
@@ -3065,7 +3203,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3065
3203
|
if (idle !== void 0) return idle;
|
|
3066
3204
|
const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
|
|
3067
3205
|
for (let i = 0; i < maxRetries; i++) {
|
|
3068
|
-
await
|
|
3206
|
+
await promises.setTimeout(CONNECTION_RETRY_INTERVAL_MS);
|
|
3069
3207
|
const found = this.getIdleWorker();
|
|
3070
3208
|
if (found !== void 0) return found;
|
|
3071
3209
|
if (this.stopping) return void 0;
|
|
@@ -3082,8 +3220,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3082
3220
|
/** Pipes remote socket bidirectionally to the worker's port. */
|
|
3083
3221
|
pipeConnection(worker, remoteSocket) {
|
|
3084
3222
|
const workerSocket = net__namespace.createConnection({ port: worker.port, host: "127.0.0.1" });
|
|
3085
|
-
worker.remoteSocket
|
|
3086
|
-
worker.workerSocket = workerSocket;
|
|
3223
|
+
worker.setSockets(remoteSocket, workerSocket);
|
|
3087
3224
|
const cleanup = () => {
|
|
3088
3225
|
remoteSocket.unpipe(workerSocket);
|
|
3089
3226
|
workerSocket.unpipe(remoteSocket);
|
|
@@ -3142,21 +3279,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3142
3279
|
const createResult = this.createDcmrecv(port, tempDir);
|
|
3143
3280
|
if (!createResult.ok) return createResult;
|
|
3144
3281
|
const dcmrecv = createResult.value;
|
|
3145
|
-
const worker =
|
|
3146
|
-
dcmrecv,
|
|
3147
|
-
port,
|
|
3148
|
-
tempDir,
|
|
3149
|
-
state: "idle",
|
|
3150
|
-
associationId: void 0,
|
|
3151
|
-
associationDir: void 0,
|
|
3152
|
-
files: [],
|
|
3153
|
-
fileSizes: [],
|
|
3154
|
-
outputLines: [],
|
|
3155
|
-
pendingFiles: [],
|
|
3156
|
-
startAt: void 0,
|
|
3157
|
-
remoteSocket: void 0,
|
|
3158
|
-
workerSocket: void 0
|
|
3159
|
-
};
|
|
3282
|
+
const worker = new Worker(dcmrecv, port, tempDir);
|
|
3160
3283
|
this.wireWorkerEvents(worker);
|
|
3161
3284
|
const startResult = await dcmrecv.start();
|
|
3162
3285
|
if (!startResult.ok) {
|
|
@@ -3165,14 +3288,9 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3165
3288
|
this.workers.set(port, worker);
|
|
3166
3289
|
return ok(worker);
|
|
3167
3290
|
}
|
|
3168
|
-
/** Stops a single worker: stop process, clean temp dir, remove from pool. */
|
|
3291
|
+
/** Stops a single worker: destroy sockets, stop process, clean temp dir, remove from pool. */
|
|
3169
3292
|
async stopWorker(worker) {
|
|
3170
|
-
|
|
3171
|
-
worker.remoteSocket.destroy();
|
|
3172
|
-
}
|
|
3173
|
-
if (worker.workerSocket !== void 0 && !worker.workerSocket.destroyed) {
|
|
3174
|
-
worker.workerSocket.destroy();
|
|
3175
|
-
}
|
|
3293
|
+
worker.destroySockets();
|
|
3176
3294
|
await worker.dcmrecv.stop();
|
|
3177
3295
|
worker.dcmrecv[Symbol.dispose]();
|
|
3178
3296
|
await removeDirSafe(worker.tempDir);
|
|
@@ -3224,12 +3342,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3224
3342
|
this.wireRefusingAssociation(worker);
|
|
3225
3343
|
this.wireOutputCapture(worker);
|
|
3226
3344
|
}
|
|
3227
|
-
/** Wires FILE_RECEIVED from dcmrecv worker —
|
|
3345
|
+
/** Wires FILE_RECEIVED from dcmrecv worker — captures context synchronously. */
|
|
3228
3346
|
wireFileReceived(worker) {
|
|
3229
3347
|
worker.dcmrecv.onFileReceived((data) => {
|
|
3230
|
-
const
|
|
3231
|
-
|
|
3232
|
-
if (assocDir === void 0 || assocId === void 0) {
|
|
3348
|
+
const ctx = worker.context;
|
|
3349
|
+
if (ctx === void 0) {
|
|
3233
3350
|
this.emit("error", {
|
|
3234
3351
|
error: new Error(`DicomReceiver: FILE_RECEIVED with no active association (worker state: ${worker.state})`),
|
|
3235
3352
|
filePath: data.filePath,
|
|
@@ -3239,21 +3356,21 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3239
3356
|
});
|
|
3240
3357
|
return;
|
|
3241
3358
|
}
|
|
3242
|
-
const promise = this.handleFileReceived(worker, data,
|
|
3243
|
-
worker.
|
|
3359
|
+
const promise = this.handleFileReceived(worker, data, ctx);
|
|
3360
|
+
worker.trackFile(promise);
|
|
3244
3361
|
});
|
|
3245
3362
|
}
|
|
3246
3363
|
/** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
|
|
3247
|
-
async handleFileReceived(worker, data,
|
|
3364
|
+
async handleFileReceived(worker, data, ctx) {
|
|
3248
3365
|
try {
|
|
3249
|
-
await this.moveAndEmitFile(worker, data,
|
|
3366
|
+
await this.moveAndEmitFile(worker, data, ctx);
|
|
3250
3367
|
} catch (thrown) {
|
|
3251
3368
|
const error = thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
3252
3369
|
this.emit("error", {
|
|
3253
3370
|
error,
|
|
3254
3371
|
filePath: data.filePath,
|
|
3255
|
-
associationId:
|
|
3256
|
-
associationDir:
|
|
3372
|
+
associationId: ctx.associationId,
|
|
3373
|
+
associationDir: ctx.associationDir,
|
|
3257
3374
|
callingAE: data.callingAE,
|
|
3258
3375
|
calledAE: data.calledAE,
|
|
3259
3376
|
source: data.source
|
|
@@ -3261,21 +3378,20 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3261
3378
|
}
|
|
3262
3379
|
}
|
|
3263
3380
|
/** Inner handler: move file, parse DICOM, emit FILE_RECEIVED or error. */
|
|
3264
|
-
async moveAndEmitFile(worker, data,
|
|
3381
|
+
async moveAndEmitFile(worker, data, ctx) {
|
|
3265
3382
|
const srcPath = data.filePath;
|
|
3266
|
-
const destPath = path__namespace.join(
|
|
3383
|
+
const destPath = path__namespace.join(ctx.associationDir, path__namespace.basename(srcPath));
|
|
3267
3384
|
const moveResult = await moveFile(srcPath, destPath);
|
|
3268
3385
|
const finalPath = moveResult.ok ? destPath : srcPath;
|
|
3269
3386
|
const fileSize = await statFileSafe(finalPath);
|
|
3270
|
-
worker.
|
|
3271
|
-
worker.fileSizes.push(fileSize);
|
|
3387
|
+
worker.recordFile(finalPath, fileSize);
|
|
3272
3388
|
const openResult = await DicomInstance.open(finalPath);
|
|
3273
3389
|
if (!openResult.ok) {
|
|
3274
3390
|
this.emit("error", {
|
|
3275
3391
|
error: openResult.error,
|
|
3276
3392
|
filePath: finalPath,
|
|
3277
|
-
associationId:
|
|
3278
|
-
associationDir:
|
|
3393
|
+
associationId: ctx.associationId,
|
|
3394
|
+
associationDir: ctx.associationDir,
|
|
3279
3395
|
callingAE: data.callingAE,
|
|
3280
3396
|
calledAE: data.calledAE,
|
|
3281
3397
|
source: data.source
|
|
@@ -3285,8 +3401,8 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3285
3401
|
this.emit("FILE_RECEIVED", {
|
|
3286
3402
|
filePath: finalPath,
|
|
3287
3403
|
fileSize,
|
|
3288
|
-
associationId:
|
|
3289
|
-
associationDir:
|
|
3404
|
+
associationId: ctx.associationId,
|
|
3405
|
+
associationDir: ctx.associationDir,
|
|
3290
3406
|
callingAE: data.callingAE,
|
|
3291
3407
|
calledAE: data.calledAE,
|
|
3292
3408
|
source: data.source,
|
|
@@ -3301,21 +3417,20 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3301
3417
|
}
|
|
3302
3418
|
/** Awaits ALL pending file operations, emits ASSOCIATION_COMPLETE, resets worker state. */
|
|
3303
3419
|
async finalizeAssociation(worker, data) {
|
|
3304
|
-
|
|
3305
|
-
await Promise.all(worker.pendingFiles);
|
|
3306
|
-
}
|
|
3420
|
+
await worker.drainPendingFiles();
|
|
3307
3421
|
this.emitAssociationComplete(worker, data);
|
|
3308
|
-
|
|
3422
|
+
worker.endAssociation();
|
|
3309
3423
|
void this.scaleDown();
|
|
3310
3424
|
}
|
|
3311
3425
|
/** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
|
|
3312
3426
|
emitAssociationComplete(worker, data) {
|
|
3313
|
-
const
|
|
3314
|
-
const
|
|
3427
|
+
const ctx = worker.context;
|
|
3428
|
+
const assocId = ctx?.associationId ?? data.associationId;
|
|
3429
|
+
const assocDir = ctx?.associationDir ?? "";
|
|
3430
|
+
const startAt = ctx?.startAt ?? Date.now();
|
|
3315
3431
|
const files = [...worker.files];
|
|
3316
3432
|
const output = [...worker.outputLines];
|
|
3317
3433
|
const endAt = Date.now();
|
|
3318
|
-
const startAt = worker.startAt ?? endAt;
|
|
3319
3434
|
const totalBytes = sumArray(worker.fileSizes);
|
|
3320
3435
|
const elapsedMs = endAt - startAt;
|
|
3321
3436
|
const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
|
|
@@ -3335,24 +3450,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3335
3450
|
output
|
|
3336
3451
|
});
|
|
3337
3452
|
}
|
|
3338
|
-
/** Resets worker state to idle after an association completes. */
|
|
3339
|
-
resetWorker(worker) {
|
|
3340
|
-
worker.state = "idle";
|
|
3341
|
-
worker.associationId = void 0;
|
|
3342
|
-
worker.associationDir = void 0;
|
|
3343
|
-
worker.files = [];
|
|
3344
|
-
worker.fileSizes = [];
|
|
3345
|
-
worker.outputLines = [];
|
|
3346
|
-
worker.pendingFiles = [];
|
|
3347
|
-
worker.startAt = void 0;
|
|
3348
|
-
worker.remoteSocket = void 0;
|
|
3349
|
-
worker.workerSocket = void 0;
|
|
3350
|
-
}
|
|
3351
3453
|
/** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
|
|
3352
3454
|
wireAssociationReceived(worker) {
|
|
3353
3455
|
worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
|
|
3354
3456
|
this.emit("ASSOCIATION_RECEIVED", {
|
|
3355
|
-
associationId: worker.associationId ?? "",
|
|
3457
|
+
associationId: worker.context?.associationId ?? "",
|
|
3356
3458
|
callingAE: data.callingAE,
|
|
3357
3459
|
calledAE: data.calledAE,
|
|
3358
3460
|
source: data.source
|
|
@@ -3363,7 +3465,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3363
3465
|
wireCStoreRequest(worker) {
|
|
3364
3466
|
worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
|
|
3365
3467
|
this.emit("C_STORE_REQUEST", {
|
|
3366
|
-
associationId: worker.associationId ?? "",
|
|
3468
|
+
associationId: worker.context?.associationId ?? "",
|
|
3367
3469
|
raw: data.raw
|
|
3368
3470
|
});
|
|
3369
3471
|
});
|
|
@@ -3372,7 +3474,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3372
3474
|
wireEchoRequest(worker) {
|
|
3373
3475
|
worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
|
|
3374
3476
|
this.emit("ECHO_REQUEST", {
|
|
3375
|
-
associationId: worker.associationId ?? ""
|
|
3477
|
+
associationId: worker.context?.associationId ?? ""
|
|
3376
3478
|
});
|
|
3377
3479
|
});
|
|
3378
3480
|
}
|
|
@@ -3387,8 +3489,8 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3387
3489
|
/** Captures worker output lines during busy associations. */
|
|
3388
3490
|
wireOutputCapture(worker) {
|
|
3389
3491
|
worker.dcmrecv.on("line", ({ text }) => {
|
|
3390
|
-
if (worker.state === "busy"
|
|
3391
|
-
worker.
|
|
3492
|
+
if (worker.state === "busy") {
|
|
3493
|
+
worker.captureOutput(text);
|
|
3392
3494
|
}
|
|
3393
3495
|
});
|
|
3394
3496
|
}
|
|
@@ -3407,56 +3509,6 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
|
|
|
3407
3509
|
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
3408
3510
|
}
|
|
3409
3511
|
};
|
|
3410
|
-
async function ensureDirectory(dirPath) {
|
|
3411
|
-
try {
|
|
3412
|
-
await fs__namespace.mkdir(dirPath, { recursive: true });
|
|
3413
|
-
return ok(void 0);
|
|
3414
|
-
} catch (e) {
|
|
3415
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3416
|
-
return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
async function moveFile(src, dest) {
|
|
3420
|
-
try {
|
|
3421
|
-
await fs__namespace.rename(src, dest);
|
|
3422
|
-
return ok(void 0);
|
|
3423
|
-
} catch {
|
|
3424
|
-
try {
|
|
3425
|
-
await fs__namespace.copyFile(src, dest);
|
|
3426
|
-
await fs__namespace.unlink(src);
|
|
3427
|
-
return ok(void 0);
|
|
3428
|
-
} catch (e) {
|
|
3429
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3430
|
-
return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
async function statFileSafe(filePath) {
|
|
3435
|
-
try {
|
|
3436
|
-
const stat3 = await fs__namespace.stat(filePath);
|
|
3437
|
-
return stat3.size;
|
|
3438
|
-
} catch {
|
|
3439
|
-
return 0;
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
function sumArray(arr) {
|
|
3443
|
-
let total = 0;
|
|
3444
|
-
for (let i = 0; i < arr.length; i++) {
|
|
3445
|
-
total += arr[i] ?? 0;
|
|
3446
|
-
}
|
|
3447
|
-
return total;
|
|
3448
|
-
}
|
|
3449
|
-
async function removeDirSafe(dirPath) {
|
|
3450
|
-
try {
|
|
3451
|
-
await fs__namespace.rm(dirPath, { recursive: true, force: true });
|
|
3452
|
-
} catch {
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
function delay(ms) {
|
|
3456
|
-
return new Promise((resolve) => {
|
|
3457
|
-
setTimeout(resolve, ms);
|
|
3458
|
-
});
|
|
3459
|
-
}
|
|
3460
3512
|
|
|
3461
3513
|
// src/events/dcmprscp.ts
|
|
3462
3514
|
var DcmprscpEvent = {
|