@ubercode/dcmtk 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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$1 = require('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$1.existsSync(path.join(dir, binaryName(bin)))) {
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) {
@@ -1904,80 +1721,301 @@ var DicomDataset = class _DicomDataset {
1904
1721
  return traversePath(this.data, segments);
1905
1722
  }
1906
1723
  /**
1907
- * Finds all values matching a path with wildcard `[*]` indices.
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);
1875
+ }
1876
+ /**
1877
+ * Marks a tag for erasure, returning a new ChangeSet.
1908
1878
  *
1909
- * Traverses all items in wildcard sequence positions using an iterative BFS queue.
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 - A branded DicomTagPath, e.g. `(0040,A730)[*].(0040,A160)`
1915
- * @returns A readonly array of all matching values (may be empty)
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
- findValues(path2) {
1918
- let segments;
1919
- try {
1920
- segments = tagPathToSegments(path2);
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
- const result = collectWildcard(this.data, segments);
1925
- return result.values;
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
- * Walks all tags in this dataset, optionally filtering by VR and recursing into sequences.
1898
+ * Marks all private tags for erasure, returning a new ChangeSet.
1929
1899
  *
1930
- * @param options - Optional VR filter and max depth
1931
- * @returns A readonly array of all matching tag entries
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
- walkTags(options) {
1934
- return walkTags(this.data, options);
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 readonly getters
1913
+ // Convenience setters for common DICOM tags
1938
1914
  // -----------------------------------------------------------------------
1939
- /** Accession Number (0008,0050). */
1940
- get accession() {
1941
- return this.getString(TAGS.AccessionNumber);
1915
+ /** Sets Patient's Name (0010,0010). */
1916
+ setPatientName(value) {
1917
+ return this.setTag("(0010,0010)", value);
1942
1918
  }
1943
- /** Patient's Name (0010,0010). */
1944
- get patientName() {
1945
- return this.getString(TAGS.PatientName);
1919
+ /** Sets Patient ID (0010,0020). */
1920
+ setPatientID(value) {
1921
+ return this.setTag("(0010,0020)", value);
1946
1922
  }
1947
- /** Patient ID (0010,0020). */
1948
- get patientID() {
1949
- return this.getString(TAGS.PatientID);
1923
+ /** Sets Study Date (0008,0020). */
1924
+ setStudyDate(value) {
1925
+ return this.setTag("(0008,0020)", value);
1950
1926
  }
1951
- /** Study Date (0008,0020). */
1952
- get studyDate() {
1953
- return this.getString(TAGS.StudyDate);
1927
+ /** Sets Modality (0008,0060). */
1928
+ setModality(value) {
1929
+ return this.setTag("(0008,0060)", value);
1954
1930
  }
1955
- /** Modality (0008,0060). */
1956
- get modality() {
1957
- return this.getString(TAGS.Modality);
1931
+ /** Sets Accession Number (0008,0050). */
1932
+ setAccessionNumber(value) {
1933
+ return this.setTag("(0008,0050)", value);
1958
1934
  }
1959
- /** SOP Class UID (0008,0016) as a branded SOPClassUID, or undefined if missing/invalid. */
1960
- get sopClassUID() {
1961
- const uid = this.getString(TAGS.SOPClassUID);
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
- /** Study Instance UID (0020,000D). */
1967
- get studyInstanceUID() {
1968
- return this.getString(TAGS.StudyInstanceUID);
1939
+ /** Sets Series Description (0008,103E). */
1940
+ setSeriesDescription(value) {
1941
+ return this.setTag("(0008,103E)", value);
1969
1942
  }
1970
- /** Series Instance UID (0020,000E). */
1971
- get seriesInstanceUID() {
1972
- return this.getString(TAGS.SeriesInstanceUID);
1943
+ /** Sets Institution Name (0008,0080). */
1944
+ setInstitutionName(value) {
1945
+ return this.setTag("(0008,0080)", value);
1973
1946
  }
1974
- /** SOP Instance UID (0008,0018). */
1975
- get sopInstanceUID() {
1976
- return this.getString(TAGS.SOPInstanceUID);
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
- /** Transfer Syntax UID (0002,0010). */
1979
- get transferSyntaxUID() {
1980
- return this.getString(TAGS.TransferSyntaxUID);
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) {
@@ -2314,12 +2352,12 @@ async function tryDirectPath(inputPath, timeoutMs, signal, verbosity) {
2314
2352
  const execOpts = signal !== void 0 ? { timeoutMs, signal } : { timeoutMs };
2315
2353
  return await execAndParse(jsonBinary.value, directArgs, inputPath, execOpts);
2316
2354
  } finally {
2317
- fs.rm(bulkDir, { recursive: true, force: true }).catch(() => {
2355
+ promises$1.rm(bulkDir, { recursive: true, force: true }).catch(() => {
2318
2356
  });
2319
2357
  }
2320
2358
  }
2321
2359
  async function createBulkTempDir() {
2322
- return fs.mkdtemp(path.join(os.tmpdir(), "dcm2json-bulk-"));
2360
+ return promises$1.mkdtemp(path.join(os.tmpdir(), "dcm2json-bulk-"));
2323
2361
  }
2324
2362
  async function execAndParse(binary, args, inputPath, execOpts) {
2325
2363
  const result = await execCommand(binary, args, execOpts);
@@ -2441,19 +2479,19 @@ async function applyModifications(filePath, changeset, options) {
2441
2479
  }
2442
2480
  async function copyFileSafe(source, dest) {
2443
2481
  return stderrLib.tryCatch(
2444
- () => fs.copyFile(source, dest),
2482
+ () => promises$1.copyFile(source, dest),
2445
2483
  (e) => new Error(`Failed to copy file: ${e.message}`)
2446
2484
  );
2447
2485
  }
2448
2486
  async function statFileSize(path2) {
2449
2487
  return stderrLib.tryCatch(
2450
- async () => (await fs.stat(path2)).size,
2488
+ async () => (await promises$1.stat(path2)).size,
2451
2489
  (e) => new Error(`Failed to stat file: ${e.message}`)
2452
2490
  );
2453
2491
  }
2454
2492
  async function unlinkFile(path2) {
2455
2493
  return stderrLib.tryCatch(
2456
- () => fs.unlink(path2),
2494
+ () => promises$1.unlink(path2),
2457
2495
  (e) => new Error(`Failed to delete file: ${e.message}`)
2458
2496
  );
2459
2497
  }
@@ -2808,6 +2846,102 @@ var DicomReceiverOptionsSchema = zod.z.object({
2808
2846
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
2809
2847
  message: "minPoolSize must be <= maxPoolSize"
2810
2848
  });
2849
+ var Worker = class {
2850
+ constructor(dcmrecv, port, tempDir) {
2851
+ __publicField(this, "dcmrecv");
2852
+ __publicField(this, "port");
2853
+ __publicField(this, "tempDir");
2854
+ __publicField(this, "_state", "idle");
2855
+ __publicField(this, "_context");
2856
+ __publicField(this, "_pending", /* @__PURE__ */ new Set());
2857
+ __publicField(this, "_files", []);
2858
+ __publicField(this, "_fileSizes", []);
2859
+ __publicField(this, "_outputLines", []);
2860
+ __publicField(this, "_remoteSocket");
2861
+ __publicField(this, "_workerSocket");
2862
+ this.dcmrecv = dcmrecv;
2863
+ this.port = port;
2864
+ this.tempDir = tempDir;
2865
+ }
2866
+ /** Current worker state. */
2867
+ get state() {
2868
+ return this._state;
2869
+ }
2870
+ /** Active association context, or undefined when idle. */
2871
+ get context() {
2872
+ return this._context;
2873
+ }
2874
+ /** Files moved to the association directory during the current association. */
2875
+ get files() {
2876
+ return this._files;
2877
+ }
2878
+ /** Byte sizes parallel to files[], for transfer stats. */
2879
+ get fileSizes() {
2880
+ return this._fileSizes;
2881
+ }
2882
+ /** Captured output lines during the current association. */
2883
+ get outputLines() {
2884
+ return this._outputLines;
2885
+ }
2886
+ /** Marks the worker busy with a new association context. */
2887
+ beginAssociation(ctx) {
2888
+ this._state = "busy";
2889
+ this._context = ctx;
2890
+ this._files.length = 0;
2891
+ this._fileSizes.length = 0;
2892
+ this._outputLines.length = 0;
2893
+ this._pending.clear();
2894
+ }
2895
+ /** Returns the worker to idle and clears all association state. */
2896
+ endAssociation() {
2897
+ this._state = "idle";
2898
+ this._context = void 0;
2899
+ this._files.length = 0;
2900
+ this._fileSizes.length = 0;
2901
+ this._outputLines.length = 0;
2902
+ this._pending.clear();
2903
+ this._remoteSocket = void 0;
2904
+ this._workerSocket = void 0;
2905
+ }
2906
+ /** Tracks an in-flight file handling promise; auto-removes on completion. */
2907
+ trackFile(promise) {
2908
+ this._pending.add(promise);
2909
+ void promise.finally(() => {
2910
+ this._pending.delete(promise);
2911
+ });
2912
+ }
2913
+ /** Records a successfully received file path and its size. */
2914
+ recordFile(filePath, size) {
2915
+ this._files.push(filePath);
2916
+ this._fileSizes.push(size);
2917
+ }
2918
+ /** Awaits all in-flight file handling promises. */
2919
+ async drainPendingFiles() {
2920
+ if (this._pending.size > 0) {
2921
+ await Promise.all(this._pending);
2922
+ }
2923
+ }
2924
+ /** Appends a line to the output buffer, respecting the cap. */
2925
+ captureOutput(text) {
2926
+ if (this._outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
2927
+ this._outputLines.push(text);
2928
+ }
2929
+ }
2930
+ /** Stores the remote and worker sockets for later cleanup. */
2931
+ setSockets(remote, worker) {
2932
+ this._remoteSocket = remote;
2933
+ this._workerSocket = worker;
2934
+ }
2935
+ /** Destroys both sockets if they exist and are not already destroyed. */
2936
+ destroySockets() {
2937
+ if (this._remoteSocket !== void 0 && !this._remoteSocket.destroyed) {
2938
+ this._remoteSocket.destroy();
2939
+ }
2940
+ if (this._workerSocket !== void 0 && !this._workerSocket.destroyed) {
2941
+ this._workerSocket.destroy();
2942
+ }
2943
+ }
2944
+ };
2811
2945
  function allocatePort() {
2812
2946
  return new Promise((resolve) => {
2813
2947
  const server = net__namespace.createServer();
@@ -2825,6 +2959,13 @@ function allocatePort() {
2825
2959
  });
2826
2960
  });
2827
2961
  }
2962
+ function sumArray(arr) {
2963
+ let total = 0;
2964
+ for (let i = 0; i < arr.length; i++) {
2965
+ total += arr[i] ?? 0;
2966
+ }
2967
+ return total;
2968
+ }
2828
2969
  var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2829
2970
  constructor(options) {
2830
2971
  super();
@@ -2839,8 +2980,6 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2839
2980
  __publicField(this, "stopping", false);
2840
2981
  __publicField(this, "abortHandler");
2841
2982
  this.setMaxListeners(20);
2842
- this.on("error", () => {
2843
- });
2844
2983
  this.options = options;
2845
2984
  this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
2846
2985
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
@@ -3048,14 +3187,12 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3048
3187
  this.emit("error", { error: mkdirResult.error });
3049
3188
  return;
3050
3189
  }
3051
- worker.state = "busy";
3052
- worker.associationId = associationId;
3053
- worker.associationDir = associationDir;
3054
- worker.files = [];
3055
- worker.fileSizes = [];
3056
- worker.outputLines = [];
3057
- worker.pendingFiles = [];
3058
- worker.startAt = Date.now();
3190
+ const ctx = {
3191
+ associationId,
3192
+ associationDir,
3193
+ startAt: Date.now()
3194
+ };
3195
+ worker.beginAssociation(ctx);
3059
3196
  this.pipeConnection(worker, remoteSocket);
3060
3197
  void this.replenishPool();
3061
3198
  }
@@ -3065,7 +3202,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3065
3202
  if (idle !== void 0) return idle;
3066
3203
  const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
3067
3204
  for (let i = 0; i < maxRetries; i++) {
3068
- await delay(CONNECTION_RETRY_INTERVAL_MS);
3205
+ await promises.setTimeout(CONNECTION_RETRY_INTERVAL_MS);
3069
3206
  const found = this.getIdleWorker();
3070
3207
  if (found !== void 0) return found;
3071
3208
  if (this.stopping) return void 0;
@@ -3082,8 +3219,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3082
3219
  /** Pipes remote socket bidirectionally to the worker's port. */
3083
3220
  pipeConnection(worker, remoteSocket) {
3084
3221
  const workerSocket = net__namespace.createConnection({ port: worker.port, host: "127.0.0.1" });
3085
- worker.remoteSocket = remoteSocket;
3086
- worker.workerSocket = workerSocket;
3222
+ worker.setSockets(remoteSocket, workerSocket);
3087
3223
  const cleanup = () => {
3088
3224
  remoteSocket.unpipe(workerSocket);
3089
3225
  workerSocket.unpipe(remoteSocket);
@@ -3142,21 +3278,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3142
3278
  const createResult = this.createDcmrecv(port, tempDir);
3143
3279
  if (!createResult.ok) return createResult;
3144
3280
  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
- };
3281
+ const worker = new Worker(dcmrecv, port, tempDir);
3160
3282
  this.wireWorkerEvents(worker);
3161
3283
  const startResult = await dcmrecv.start();
3162
3284
  if (!startResult.ok) {
@@ -3165,14 +3287,9 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3165
3287
  this.workers.set(port, worker);
3166
3288
  return ok(worker);
3167
3289
  }
3168
- /** Stops a single worker: stop process, clean temp dir, remove from pool. */
3290
+ /** Stops a single worker: destroy sockets, stop process, clean temp dir, remove from pool. */
3169
3291
  async stopWorker(worker) {
3170
- if (worker.remoteSocket !== void 0 && !worker.remoteSocket.destroyed) {
3171
- worker.remoteSocket.destroy();
3172
- }
3173
- if (worker.workerSocket !== void 0 && !worker.workerSocket.destroyed) {
3174
- worker.workerSocket.destroy();
3175
- }
3292
+ worker.destroySockets();
3176
3293
  await worker.dcmrecv.stop();
3177
3294
  worker.dcmrecv[Symbol.dispose]();
3178
3295
  await removeDirSafe(worker.tempDir);
@@ -3224,12 +3341,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3224
3341
  this.wireRefusingAssociation(worker);
3225
3342
  this.wireOutputCapture(worker);
3226
3343
  }
3227
- /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
3344
+ /** Wires FILE_RECEIVED from dcmrecv worker captures context synchronously. */
3228
3345
  wireFileReceived(worker) {
3229
3346
  worker.dcmrecv.onFileReceived((data) => {
3230
- const assocId = worker.associationId;
3231
- const assocDir = worker.associationDir;
3232
- if (assocDir === void 0 || assocId === void 0) {
3347
+ const ctx = worker.context;
3348
+ if (ctx === void 0) {
3233
3349
  this.emit("error", {
3234
3350
  error: new Error(`DicomReceiver: FILE_RECEIVED with no active association (worker state: ${worker.state})`),
3235
3351
  filePath: data.filePath,
@@ -3239,22 +3355,21 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3239
3355
  });
3240
3356
  return;
3241
3357
  }
3242
- const promise = this.handleFileReceived(worker, data, assocId, assocDir);
3243
- worker.pendingFiles.push(promise);
3244
- void promise.finally(() => removePending(worker.pendingFiles, promise));
3358
+ const promise = this.handleFileReceived(worker, data, ctx);
3359
+ worker.trackFile(promise);
3245
3360
  });
3246
3361
  }
3247
3362
  /** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
3248
- async handleFileReceived(worker, data, assocId, assocDir) {
3363
+ async handleFileReceived(worker, data, ctx) {
3249
3364
  try {
3250
- await this.moveAndEmitFile(worker, data, assocId, assocDir);
3365
+ await this.moveAndEmitFile(worker, data, ctx);
3251
3366
  } catch (thrown) {
3252
3367
  const error = thrown instanceof Error ? thrown : new Error(String(thrown));
3253
3368
  this.emit("error", {
3254
3369
  error,
3255
3370
  filePath: data.filePath,
3256
- associationId: assocId,
3257
- associationDir: assocDir,
3371
+ associationId: ctx.associationId,
3372
+ associationDir: ctx.associationDir,
3258
3373
  callingAE: data.callingAE,
3259
3374
  calledAE: data.calledAE,
3260
3375
  source: data.source
@@ -3262,21 +3377,20 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3262
3377
  }
3263
3378
  }
3264
3379
  /** Inner handler: move file, parse DICOM, emit FILE_RECEIVED or error. */
3265
- async moveAndEmitFile(worker, data, assocId, assocDir) {
3380
+ async moveAndEmitFile(worker, data, ctx) {
3266
3381
  const srcPath = data.filePath;
3267
- const destPath = path__namespace.join(assocDir, path__namespace.basename(srcPath));
3382
+ const destPath = path__namespace.join(ctx.associationDir, path__namespace.basename(srcPath));
3268
3383
  const moveResult = await moveFile(srcPath, destPath);
3269
3384
  const finalPath = moveResult.ok ? destPath : srcPath;
3270
3385
  const fileSize = await statFileSafe(finalPath);
3271
- worker.files.push(finalPath);
3272
- worker.fileSizes.push(fileSize);
3386
+ worker.recordFile(finalPath, fileSize);
3273
3387
  const openResult = await DicomInstance.open(finalPath);
3274
3388
  if (!openResult.ok) {
3275
3389
  this.emit("error", {
3276
3390
  error: openResult.error,
3277
3391
  filePath: finalPath,
3278
- associationId: assocId,
3279
- associationDir: assocDir,
3392
+ associationId: ctx.associationId,
3393
+ associationDir: ctx.associationDir,
3280
3394
  callingAE: data.callingAE,
3281
3395
  calledAE: data.calledAE,
3282
3396
  source: data.source
@@ -3286,8 +3400,8 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3286
3400
  this.emit("FILE_RECEIVED", {
3287
3401
  filePath: finalPath,
3288
3402
  fileSize,
3289
- associationId: assocId,
3290
- associationDir: assocDir,
3403
+ associationId: ctx.associationId,
3404
+ associationDir: ctx.associationDir,
3291
3405
  callingAE: data.callingAE,
3292
3406
  calledAE: data.calledAE,
3293
3407
  source: data.source,
@@ -3302,21 +3416,20 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3302
3416
  }
3303
3417
  /** Awaits ALL pending file operations, emits ASSOCIATION_COMPLETE, resets worker state. */
3304
3418
  async finalizeAssociation(worker, data) {
3305
- while (worker.pendingFiles.length > 0) {
3306
- await Promise.all(worker.pendingFiles);
3307
- }
3419
+ await worker.drainPendingFiles();
3308
3420
  this.emitAssociationComplete(worker, data);
3309
- this.resetWorker(worker);
3421
+ worker.endAssociation();
3310
3422
  void this.scaleDown();
3311
3423
  }
3312
3424
  /** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
3313
3425
  emitAssociationComplete(worker, data) {
3314
- const assocId = worker.associationId ?? data.associationId;
3315
- const assocDir = worker.associationDir ?? "";
3426
+ const ctx = worker.context;
3427
+ const assocId = ctx?.associationId ?? data.associationId;
3428
+ const assocDir = ctx?.associationDir ?? "";
3429
+ const startAt = ctx?.startAt ?? Date.now();
3316
3430
  const files = [...worker.files];
3317
3431
  const output = [...worker.outputLines];
3318
3432
  const endAt = Date.now();
3319
- const startAt = worker.startAt ?? endAt;
3320
3433
  const totalBytes = sumArray(worker.fileSizes);
3321
3434
  const elapsedMs = endAt - startAt;
3322
3435
  const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
@@ -3336,24 +3449,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3336
3449
  output
3337
3450
  });
3338
3451
  }
3339
- /** Resets worker state to idle after an association completes. */
3340
- resetWorker(worker) {
3341
- worker.state = "idle";
3342
- worker.associationId = void 0;
3343
- worker.associationDir = void 0;
3344
- worker.files = [];
3345
- worker.fileSizes = [];
3346
- worker.outputLines = [];
3347
- worker.pendingFiles = [];
3348
- worker.startAt = void 0;
3349
- worker.remoteSocket = void 0;
3350
- worker.workerSocket = void 0;
3351
- }
3352
3452
  /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
3353
3453
  wireAssociationReceived(worker) {
3354
3454
  worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
3355
3455
  this.emit("ASSOCIATION_RECEIVED", {
3356
- associationId: worker.associationId ?? "",
3456
+ associationId: worker.context?.associationId ?? "",
3357
3457
  callingAE: data.callingAE,
3358
3458
  calledAE: data.calledAE,
3359
3459
  source: data.source
@@ -3364,7 +3464,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3364
3464
  wireCStoreRequest(worker) {
3365
3465
  worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
3366
3466
  this.emit("C_STORE_REQUEST", {
3367
- associationId: worker.associationId ?? "",
3467
+ associationId: worker.context?.associationId ?? "",
3368
3468
  raw: data.raw
3369
3469
  });
3370
3470
  });
@@ -3373,7 +3473,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3373
3473
  wireEchoRequest(worker) {
3374
3474
  worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
3375
3475
  this.emit("ECHO_REQUEST", {
3376
- associationId: worker.associationId ?? ""
3476
+ associationId: worker.context?.associationId ?? ""
3377
3477
  });
3378
3478
  });
3379
3479
  }
@@ -3388,8 +3488,8 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3388
3488
  /** Captures worker output lines during busy associations. */
3389
3489
  wireOutputCapture(worker) {
3390
3490
  worker.dcmrecv.on("line", ({ text }) => {
3391
- if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
3392
- worker.outputLines.push(text);
3491
+ if (worker.state === "busy") {
3492
+ worker.captureOutput(text);
3393
3493
  }
3394
3494
  });
3395
3495
  }
@@ -3408,60 +3508,6 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3408
3508
  signal.addEventListener("abort", this.abortHandler, { once: true });
3409
3509
  }
3410
3510
  };
3411
- async function ensureDirectory(dirPath) {
3412
- try {
3413
- await fs__namespace.mkdir(dirPath, { recursive: true });
3414
- return ok(void 0);
3415
- } catch (e) {
3416
- const msg = e instanceof Error ? e.message : String(e);
3417
- return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
3418
- }
3419
- }
3420
- async function moveFile(src, dest) {
3421
- try {
3422
- await fs__namespace.rename(src, dest);
3423
- return ok(void 0);
3424
- } catch {
3425
- try {
3426
- await fs__namespace.copyFile(src, dest);
3427
- await fs__namespace.unlink(src);
3428
- return ok(void 0);
3429
- } catch (e) {
3430
- const msg = e instanceof Error ? e.message : String(e);
3431
- return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
3432
- }
3433
- }
3434
- }
3435
- async function statFileSafe(filePath) {
3436
- try {
3437
- const stat3 = await fs__namespace.stat(filePath);
3438
- return stat3.size;
3439
- } catch {
3440
- return 0;
3441
- }
3442
- }
3443
- function sumArray(arr) {
3444
- let total = 0;
3445
- for (let i = 0; i < arr.length; i++) {
3446
- total += arr[i] ?? 0;
3447
- }
3448
- return total;
3449
- }
3450
- async function removeDirSafe(dirPath) {
3451
- try {
3452
- await fs__namespace.rm(dirPath, { recursive: true, force: true });
3453
- } catch {
3454
- }
3455
- }
3456
- function removePending(arr, promise) {
3457
- const idx = arr.indexOf(promise);
3458
- if (idx !== -1) void arr.splice(idx, 1);
3459
- }
3460
- function delay(ms) {
3461
- return new Promise((resolve) => {
3462
- setTimeout(resolve, ms);
3463
- });
3464
- }
3465
3511
 
3466
3512
  // src/events/dcmprscp.ts
3467
3513
  var DcmprscpEvent = {