@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/index.cjs CHANGED
@@ -2,15 +2,16 @@
2
2
 
3
3
  var path = require('path');
4
4
  var zod = require('zod');
5
- var fs$1 = require('fs');
5
+ var fs = require('fs');
6
6
  var child_process = require('child_process');
7
7
  var kill = require('tree-kill');
8
8
  var events = require('events');
9
9
  var stderrLib = require('stderr-lib');
10
- var fs = require('fs/promises');
10
+ var promises$1 = require('fs/promises');
11
11
  var os = require('os');
12
12
  var fastXmlParser = require('fast-xml-parser');
13
13
  var net = require('net');
14
+ var promises = require('timers/promises');
14
15
  var crypto = require('crypto');
15
16
 
16
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -35,7 +36,6 @@ function _interopNamespace(e) {
35
36
 
36
37
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
37
38
  var kill__default = /*#__PURE__*/_interopDefault(kill);
38
- var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
39
39
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
40
40
  var net__namespace = /*#__PURE__*/_interopNamespace(net);
41
41
 
@@ -224,7 +224,7 @@ function binaryName(name) {
224
224
  }
225
225
  function hasRequiredBinaries(dir) {
226
226
  for (const bin of REQUIRED_BINARIES) {
227
- if (!fs$1.existsSync(path.join(dir, binaryName(bin)))) {
227
+ if (!fs.existsSync(path.join(dir, binaryName(bin)))) {
228
228
  return false;
229
229
  }
230
230
  }
@@ -31508,12 +31508,12 @@ async function tryDirectPath(inputPath, timeoutMs, signal, verbosity) {
31508
31508
  const execOpts = signal !== void 0 ? { timeoutMs, signal } : { timeoutMs };
31509
31509
  return await execAndParse(jsonBinary.value, directArgs, inputPath, execOpts);
31510
31510
  } finally {
31511
- fs.rm(bulkDir, { recursive: true, force: true }).catch(() => {
31511
+ promises$1.rm(bulkDir, { recursive: true, force: true }).catch(() => {
31512
31512
  });
31513
31513
  }
31514
31514
  }
31515
31515
  async function createBulkTempDir() {
31516
- return fs.mkdtemp(path.join(os.tmpdir(), "dcm2json-bulk-"));
31516
+ return promises$1.mkdtemp(path.join(os.tmpdir(), "dcm2json-bulk-"));
31517
31517
  }
31518
31518
  async function execAndParse(binary, args, inputPath, execOpts) {
31519
31519
  const result = await execCommand(binary, args, execOpts);
@@ -34341,19 +34341,19 @@ async function applyModifications(filePath, changeset, options) {
34341
34341
  }
34342
34342
  async function copyFileSafe(source, dest) {
34343
34343
  return stderrLib.tryCatch(
34344
- () => fs.copyFile(source, dest),
34344
+ () => promises$1.copyFile(source, dest),
34345
34345
  (e) => new Error(`Failed to copy file: ${e.message}`)
34346
34346
  );
34347
34347
  }
34348
34348
  async function statFileSize(path2) {
34349
34349
  return stderrLib.tryCatch(
34350
- async () => (await fs.stat(path2)).size,
34350
+ async () => (await promises$1.stat(path2)).size,
34351
34351
  (e) => new Error(`Failed to stat file: ${e.message}`)
34352
34352
  );
34353
34353
  }
34354
34354
  async function unlinkFile(path2) {
34355
34355
  return stderrLib.tryCatch(
34356
- () => fs.unlink(path2),
34356
+ () => promises$1.unlink(path2),
34357
34357
  (e) => new Error(`Failed to delete file: ${e.message}`)
34358
34358
  );
34359
34359
  }
@@ -36272,6 +36272,184 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
36272
36272
  signal.addEventListener("abort", this.abortHandler, { once: true });
36273
36273
  }
36274
36274
  };
36275
+ var MIN_CONCURRENCY = 1;
36276
+ var MAX_CONCURRENCY = 64;
36277
+ var DEFAULT_CONCURRENCY = 4;
36278
+ function clampConcurrency(value) {
36279
+ if (value === void 0) return DEFAULT_CONCURRENCY;
36280
+ return Math.max(MIN_CONCURRENCY, Math.min(MAX_CONCURRENCY, Math.floor(value)));
36281
+ }
36282
+ function createBatchState(total) {
36283
+ const results = [];
36284
+ for (let i = 0; i < total; i += 1) {
36285
+ results.push(void 0);
36286
+ }
36287
+ return { results, succeeded: 0, failed: 0, completed: 0 };
36288
+ }
36289
+ async function processItem(item, index, operation, state) {
36290
+ let result;
36291
+ try {
36292
+ result = await operation(item);
36293
+ } catch (thrown) {
36294
+ result = err(stderrLib.stderr(thrown));
36295
+ }
36296
+ state.results[index] = result;
36297
+ state.completed += 1;
36298
+ if (result.ok) {
36299
+ state.succeeded += 1;
36300
+ } else {
36301
+ state.failed += 1;
36302
+ }
36303
+ }
36304
+ async function batch(items, operation, options) {
36305
+ const concurrency = clampConcurrency(options?.concurrency);
36306
+ const signal = options?.signal;
36307
+ const onProgress = options?.onProgress;
36308
+ const total = items.length;
36309
+ if (total === 0) {
36310
+ return { results: [], succeeded: 0, failed: 0 };
36311
+ }
36312
+ const state = createBatchState(total);
36313
+ const inFlight = /* @__PURE__ */ new Set();
36314
+ let nextIndex = 0;
36315
+ while (nextIndex < total) {
36316
+ if (signal?.aborted) break;
36317
+ const itemIndex = nextIndex;
36318
+ nextIndex += 1;
36319
+ const promise = processItem(items[itemIndex], itemIndex, operation, state).then(() => {
36320
+ if (onProgress) {
36321
+ const result = state.results[itemIndex];
36322
+ onProgress(state.completed, total, result);
36323
+ }
36324
+ });
36325
+ inFlight.add(promise);
36326
+ promise.then(
36327
+ () => inFlight.delete(promise),
36328
+ () => inFlight.delete(promise)
36329
+ );
36330
+ if (inFlight.size >= concurrency) {
36331
+ await Promise.race(inFlight);
36332
+ }
36333
+ }
36334
+ if (inFlight.size > 0) {
36335
+ await Promise.all(inFlight);
36336
+ }
36337
+ return { results: state.results, succeeded: state.succeeded, failed: state.failed };
36338
+ }
36339
+
36340
+ // src/utils/retry.ts
36341
+ var DEFAULT_MAX_ATTEMPTS = 3;
36342
+ var DEFAULT_INITIAL_DELAY_MS = 1e3;
36343
+ var DEFAULT_MAX_DELAY_MS = 3e4;
36344
+ var DEFAULT_BACKOFF_MULTIPLIER = 2;
36345
+ var JITTER_FRACTION = 0.1;
36346
+ var DEFAULT_CONFIG = {
36347
+ maxAttempts: DEFAULT_MAX_ATTEMPTS,
36348
+ initialDelayMs: DEFAULT_INITIAL_DELAY_MS,
36349
+ maxDelayMs: DEFAULT_MAX_DELAY_MS,
36350
+ backoffMultiplier: DEFAULT_BACKOFF_MULTIPLIER,
36351
+ shouldRetry: void 0,
36352
+ signal: void 0,
36353
+ onRetry: void 0
36354
+ };
36355
+ function resolveConfig(opts) {
36356
+ if (!opts) return DEFAULT_CONFIG;
36357
+ return {
36358
+ ...DEFAULT_CONFIG,
36359
+ ...Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0)),
36360
+ maxAttempts: Math.max(1, Math.floor(opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)),
36361
+ initialDelayMs: Math.max(0, opts.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS),
36362
+ maxDelayMs: Math.max(0, opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS),
36363
+ backoffMultiplier: Math.max(1, opts.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER)
36364
+ };
36365
+ }
36366
+ function computeDelay(attempt, config) {
36367
+ const baseDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
36368
+ const jitter = baseDelay * JITTER_FRACTION * (2 * Math.random() - 1);
36369
+ return Math.max(0, Math.round(baseDelay + jitter));
36370
+ }
36371
+ async function waitWithAbort(delayMs, signal) {
36372
+ if (signal?.aborted) {
36373
+ return false;
36374
+ }
36375
+ return new Promise((resolve) => {
36376
+ const timer = setTimeout(() => {
36377
+ cleanup();
36378
+ resolve(true);
36379
+ }, delayMs);
36380
+ const onAbort = () => {
36381
+ clearTimeout(timer);
36382
+ cleanup();
36383
+ resolve(false);
36384
+ };
36385
+ const cleanup = () => {
36386
+ signal?.removeEventListener("abort", onAbort);
36387
+ };
36388
+ if (signal) {
36389
+ signal.addEventListener("abort", onAbort, { once: true });
36390
+ }
36391
+ });
36392
+ }
36393
+ function shouldBreakAfterFailure(attempt, lastError, config) {
36394
+ if (attempt === config.maxAttempts - 1) return true;
36395
+ if (config.shouldRetry && !config.shouldRetry(lastError, attempt + 1)) return true;
36396
+ if (config.signal?.aborted) return true;
36397
+ return false;
36398
+ }
36399
+ async function retry(operation, options) {
36400
+ const config = resolveConfig(options);
36401
+ let lastResult = err(new Error("No attempts executed"));
36402
+ for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
36403
+ lastResult = await operation();
36404
+ if (lastResult.ok) return lastResult;
36405
+ if (shouldBreakAfterFailure(attempt, lastResult.error, config)) break;
36406
+ const delayMs = computeDelay(attempt, config);
36407
+ if (config.onRetry) config.onRetry(lastResult.error, attempt + 1, delayMs);
36408
+ const waitCompleted = await waitWithAbort(delayMs, config.signal);
36409
+ if (!waitCompleted) break;
36410
+ }
36411
+ return lastResult;
36412
+ }
36413
+ async function ensureDirectory(dirPath) {
36414
+ try {
36415
+ await promises$1.mkdir(dirPath, { recursive: true });
36416
+ return ok(void 0);
36417
+ } catch (e) {
36418
+ const msg = e instanceof Error ? e.message : String(e);
36419
+ return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
36420
+ }
36421
+ }
36422
+ async function moveFile(src, dest) {
36423
+ try {
36424
+ await promises$1.rename(src, dest);
36425
+ return ok(void 0);
36426
+ } catch {
36427
+ try {
36428
+ await promises$1.copyFile(src, dest);
36429
+ await promises$1.unlink(src);
36430
+ return ok(void 0);
36431
+ } catch (e) {
36432
+ const msg = e instanceof Error ? e.message : String(e);
36433
+ return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
36434
+ }
36435
+ }
36436
+ }
36437
+ async function statFileSafe(filePath) {
36438
+ try {
36439
+ const s = await promises$1.stat(filePath);
36440
+ return s.size;
36441
+ } catch {
36442
+ return 0;
36443
+ }
36444
+ }
36445
+ async function removeDirSafe(dirPath) {
36446
+ try {
36447
+ await promises$1.rm(dirPath, { recursive: true, force: true });
36448
+ } catch {
36449
+ }
36450
+ }
36451
+
36452
+ // src/servers/DicomReceiver.ts
36275
36453
  var DEFAULT_MIN_POOL_SIZE = 2;
36276
36454
  var DEFAULT_MAX_POOL_SIZE = 10;
36277
36455
  var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
@@ -36297,6 +36475,102 @@ var DicomReceiverOptionsSchema = zod.z.object({
36297
36475
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
36298
36476
  message: "minPoolSize must be <= maxPoolSize"
36299
36477
  });
36478
+ var Worker = class {
36479
+ constructor(dcmrecv, port, tempDir) {
36480
+ __publicField(this, "dcmrecv");
36481
+ __publicField(this, "port");
36482
+ __publicField(this, "tempDir");
36483
+ __publicField(this, "_state", "idle");
36484
+ __publicField(this, "_context");
36485
+ __publicField(this, "_pending", /* @__PURE__ */ new Set());
36486
+ __publicField(this, "_files", []);
36487
+ __publicField(this, "_fileSizes", []);
36488
+ __publicField(this, "_outputLines", []);
36489
+ __publicField(this, "_remoteSocket");
36490
+ __publicField(this, "_workerSocket");
36491
+ this.dcmrecv = dcmrecv;
36492
+ this.port = port;
36493
+ this.tempDir = tempDir;
36494
+ }
36495
+ /** Current worker state. */
36496
+ get state() {
36497
+ return this._state;
36498
+ }
36499
+ /** Active association context, or undefined when idle. */
36500
+ get context() {
36501
+ return this._context;
36502
+ }
36503
+ /** Files moved to the association directory during the current association. */
36504
+ get files() {
36505
+ return this._files;
36506
+ }
36507
+ /** Byte sizes parallel to files[], for transfer stats. */
36508
+ get fileSizes() {
36509
+ return this._fileSizes;
36510
+ }
36511
+ /** Captured output lines during the current association. */
36512
+ get outputLines() {
36513
+ return this._outputLines;
36514
+ }
36515
+ /** Marks the worker busy with a new association context. */
36516
+ beginAssociation(ctx) {
36517
+ this._state = "busy";
36518
+ this._context = ctx;
36519
+ this._files.length = 0;
36520
+ this._fileSizes.length = 0;
36521
+ this._outputLines.length = 0;
36522
+ this._pending.clear();
36523
+ }
36524
+ /** Returns the worker to idle and clears all association state. */
36525
+ endAssociation() {
36526
+ this._state = "idle";
36527
+ this._context = void 0;
36528
+ this._files.length = 0;
36529
+ this._fileSizes.length = 0;
36530
+ this._outputLines.length = 0;
36531
+ this._pending.clear();
36532
+ this._remoteSocket = void 0;
36533
+ this._workerSocket = void 0;
36534
+ }
36535
+ /** Tracks an in-flight file handling promise; auto-removes on completion. */
36536
+ trackFile(promise) {
36537
+ this._pending.add(promise);
36538
+ void promise.finally(() => {
36539
+ this._pending.delete(promise);
36540
+ });
36541
+ }
36542
+ /** Records a successfully received file path and its size. */
36543
+ recordFile(filePath, size) {
36544
+ this._files.push(filePath);
36545
+ this._fileSizes.push(size);
36546
+ }
36547
+ /** Awaits all in-flight file handling promises. */
36548
+ async drainPendingFiles() {
36549
+ if (this._pending.size > 0) {
36550
+ await Promise.all(this._pending);
36551
+ }
36552
+ }
36553
+ /** Appends a line to the output buffer, respecting the cap. */
36554
+ captureOutput(text) {
36555
+ if (this._outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
36556
+ this._outputLines.push(text);
36557
+ }
36558
+ }
36559
+ /** Stores the remote and worker sockets for later cleanup. */
36560
+ setSockets(remote, worker) {
36561
+ this._remoteSocket = remote;
36562
+ this._workerSocket = worker;
36563
+ }
36564
+ /** Destroys both sockets if they exist and are not already destroyed. */
36565
+ destroySockets() {
36566
+ if (this._remoteSocket !== void 0 && !this._remoteSocket.destroyed) {
36567
+ this._remoteSocket.destroy();
36568
+ }
36569
+ if (this._workerSocket !== void 0 && !this._workerSocket.destroyed) {
36570
+ this._workerSocket.destroy();
36571
+ }
36572
+ }
36573
+ };
36300
36574
  function allocatePort() {
36301
36575
  return new Promise((resolve) => {
36302
36576
  const server = net__namespace.createServer();
@@ -36314,6 +36588,13 @@ function allocatePort() {
36314
36588
  });
36315
36589
  });
36316
36590
  }
36591
+ function sumArray(arr) {
36592
+ let total = 0;
36593
+ for (let i = 0; i < arr.length; i++) {
36594
+ total += arr[i] ?? 0;
36595
+ }
36596
+ return total;
36597
+ }
36317
36598
  var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36318
36599
  constructor(options) {
36319
36600
  super();
@@ -36328,8 +36609,6 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36328
36609
  __publicField(this, "stopping", false);
36329
36610
  __publicField(this, "abortHandler");
36330
36611
  this.setMaxListeners(20);
36331
- this.on("error", () => {
36332
- });
36333
36612
  this.options = options;
36334
36613
  this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
36335
36614
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
@@ -36537,14 +36816,12 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36537
36816
  this.emit("error", { error: mkdirResult.error });
36538
36817
  return;
36539
36818
  }
36540
- worker.state = "busy";
36541
- worker.associationId = associationId;
36542
- worker.associationDir = associationDir;
36543
- worker.files = [];
36544
- worker.fileSizes = [];
36545
- worker.outputLines = [];
36546
- worker.pendingFiles = [];
36547
- worker.startAt = Date.now();
36819
+ const ctx = {
36820
+ associationId,
36821
+ associationDir,
36822
+ startAt: Date.now()
36823
+ };
36824
+ worker.beginAssociation(ctx);
36548
36825
  this.pipeConnection(worker, remoteSocket);
36549
36826
  void this.replenishPool();
36550
36827
  }
@@ -36554,7 +36831,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36554
36831
  if (idle !== void 0) return idle;
36555
36832
  const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
36556
36833
  for (let i = 0; i < maxRetries; i++) {
36557
- await delay(CONNECTION_RETRY_INTERVAL_MS);
36834
+ await promises.setTimeout(CONNECTION_RETRY_INTERVAL_MS);
36558
36835
  const found = this.getIdleWorker();
36559
36836
  if (found !== void 0) return found;
36560
36837
  if (this.stopping) return void 0;
@@ -36571,8 +36848,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36571
36848
  /** Pipes remote socket bidirectionally to the worker's port. */
36572
36849
  pipeConnection(worker, remoteSocket) {
36573
36850
  const workerSocket = net__namespace.createConnection({ port: worker.port, host: "127.0.0.1" });
36574
- worker.remoteSocket = remoteSocket;
36575
- worker.workerSocket = workerSocket;
36851
+ worker.setSockets(remoteSocket, workerSocket);
36576
36852
  const cleanup = () => {
36577
36853
  remoteSocket.unpipe(workerSocket);
36578
36854
  workerSocket.unpipe(remoteSocket);
@@ -36631,21 +36907,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36631
36907
  const createResult = this.createDcmrecv(port, tempDir);
36632
36908
  if (!createResult.ok) return createResult;
36633
36909
  const dcmrecv = createResult.value;
36634
- const worker = {
36635
- dcmrecv,
36636
- port,
36637
- tempDir,
36638
- state: "idle",
36639
- associationId: void 0,
36640
- associationDir: void 0,
36641
- files: [],
36642
- fileSizes: [],
36643
- outputLines: [],
36644
- pendingFiles: [],
36645
- startAt: void 0,
36646
- remoteSocket: void 0,
36647
- workerSocket: void 0
36648
- };
36910
+ const worker = new Worker(dcmrecv, port, tempDir);
36649
36911
  this.wireWorkerEvents(worker);
36650
36912
  const startResult = await dcmrecv.start();
36651
36913
  if (!startResult.ok) {
@@ -36654,14 +36916,9 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36654
36916
  this.workers.set(port, worker);
36655
36917
  return ok(worker);
36656
36918
  }
36657
- /** Stops a single worker: stop process, clean temp dir, remove from pool. */
36919
+ /** Stops a single worker: destroy sockets, stop process, clean temp dir, remove from pool. */
36658
36920
  async stopWorker(worker) {
36659
- if (worker.remoteSocket !== void 0 && !worker.remoteSocket.destroyed) {
36660
- worker.remoteSocket.destroy();
36661
- }
36662
- if (worker.workerSocket !== void 0 && !worker.workerSocket.destroyed) {
36663
- worker.workerSocket.destroy();
36664
- }
36921
+ worker.destroySockets();
36665
36922
  await worker.dcmrecv.stop();
36666
36923
  worker.dcmrecv[Symbol.dispose]();
36667
36924
  await removeDirSafe(worker.tempDir);
@@ -36713,12 +36970,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36713
36970
  this.wireRefusingAssociation(worker);
36714
36971
  this.wireOutputCapture(worker);
36715
36972
  }
36716
- /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
36973
+ /** Wires FILE_RECEIVED from dcmrecv worker captures context synchronously. */
36717
36974
  wireFileReceived(worker) {
36718
36975
  worker.dcmrecv.onFileReceived((data) => {
36719
- const assocId = worker.associationId;
36720
- const assocDir = worker.associationDir;
36721
- if (assocDir === void 0 || assocId === void 0) {
36976
+ const ctx = worker.context;
36977
+ if (ctx === void 0) {
36722
36978
  this.emit("error", {
36723
36979
  error: new Error(`DicomReceiver: FILE_RECEIVED with no active association (worker state: ${worker.state})`),
36724
36980
  filePath: data.filePath,
@@ -36728,22 +36984,21 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36728
36984
  });
36729
36985
  return;
36730
36986
  }
36731
- const promise = this.handleFileReceived(worker, data, assocId, assocDir);
36732
- worker.pendingFiles.push(promise);
36733
- void promise.finally(() => removePending(worker.pendingFiles, promise));
36987
+ const promise = this.handleFileReceived(worker, data, ctx);
36988
+ worker.trackFile(promise);
36734
36989
  });
36735
36990
  }
36736
36991
  /** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
36737
- async handleFileReceived(worker, data, assocId, assocDir) {
36992
+ async handleFileReceived(worker, data, ctx) {
36738
36993
  try {
36739
- await this.moveAndEmitFile(worker, data, assocId, assocDir);
36994
+ await this.moveAndEmitFile(worker, data, ctx);
36740
36995
  } catch (thrown) {
36741
36996
  const error = thrown instanceof Error ? thrown : new Error(String(thrown));
36742
36997
  this.emit("error", {
36743
36998
  error,
36744
36999
  filePath: data.filePath,
36745
- associationId: assocId,
36746
- associationDir: assocDir,
37000
+ associationId: ctx.associationId,
37001
+ associationDir: ctx.associationDir,
36747
37002
  callingAE: data.callingAE,
36748
37003
  calledAE: data.calledAE,
36749
37004
  source: data.source
@@ -36751,21 +37006,20 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36751
37006
  }
36752
37007
  }
36753
37008
  /** Inner handler: move file, parse DICOM, emit FILE_RECEIVED or error. */
36754
- async moveAndEmitFile(worker, data, assocId, assocDir) {
37009
+ async moveAndEmitFile(worker, data, ctx) {
36755
37010
  const srcPath = data.filePath;
36756
- const destPath = path__namespace.join(assocDir, path__namespace.basename(srcPath));
37011
+ const destPath = path__namespace.join(ctx.associationDir, path__namespace.basename(srcPath));
36757
37012
  const moveResult = await moveFile(srcPath, destPath);
36758
37013
  const finalPath = moveResult.ok ? destPath : srcPath;
36759
37014
  const fileSize = await statFileSafe(finalPath);
36760
- worker.files.push(finalPath);
36761
- worker.fileSizes.push(fileSize);
37015
+ worker.recordFile(finalPath, fileSize);
36762
37016
  const openResult = await DicomInstance.open(finalPath);
36763
37017
  if (!openResult.ok) {
36764
37018
  this.emit("error", {
36765
37019
  error: openResult.error,
36766
37020
  filePath: finalPath,
36767
- associationId: assocId,
36768
- associationDir: assocDir,
37021
+ associationId: ctx.associationId,
37022
+ associationDir: ctx.associationDir,
36769
37023
  callingAE: data.callingAE,
36770
37024
  calledAE: data.calledAE,
36771
37025
  source: data.source
@@ -36775,8 +37029,8 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36775
37029
  this.emit("FILE_RECEIVED", {
36776
37030
  filePath: finalPath,
36777
37031
  fileSize,
36778
- associationId: assocId,
36779
- associationDir: assocDir,
37032
+ associationId: ctx.associationId,
37033
+ associationDir: ctx.associationDir,
36780
37034
  callingAE: data.callingAE,
36781
37035
  calledAE: data.calledAE,
36782
37036
  source: data.source,
@@ -36791,21 +37045,20 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36791
37045
  }
36792
37046
  /** Awaits ALL pending file operations, emits ASSOCIATION_COMPLETE, resets worker state. */
36793
37047
  async finalizeAssociation(worker, data) {
36794
- while (worker.pendingFiles.length > 0) {
36795
- await Promise.all(worker.pendingFiles);
36796
- }
37048
+ await worker.drainPendingFiles();
36797
37049
  this.emitAssociationComplete(worker, data);
36798
- this.resetWorker(worker);
37050
+ worker.endAssociation();
36799
37051
  void this.scaleDown();
36800
37052
  }
36801
37053
  /** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
36802
37054
  emitAssociationComplete(worker, data) {
36803
- const assocId = worker.associationId ?? data.associationId;
36804
- const assocDir = worker.associationDir ?? "";
37055
+ const ctx = worker.context;
37056
+ const assocId = ctx?.associationId ?? data.associationId;
37057
+ const assocDir = ctx?.associationDir ?? "";
37058
+ const startAt = ctx?.startAt ?? Date.now();
36805
37059
  const files = [...worker.files];
36806
37060
  const output = [...worker.outputLines];
36807
37061
  const endAt = Date.now();
36808
- const startAt = worker.startAt ?? endAt;
36809
37062
  const totalBytes = sumArray(worker.fileSizes);
36810
37063
  const elapsedMs = endAt - startAt;
36811
37064
  const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
@@ -36825,24 +37078,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36825
37078
  output
36826
37079
  });
36827
37080
  }
36828
- /** Resets worker state to idle after an association completes. */
36829
- resetWorker(worker) {
36830
- worker.state = "idle";
36831
- worker.associationId = void 0;
36832
- worker.associationDir = void 0;
36833
- worker.files = [];
36834
- worker.fileSizes = [];
36835
- worker.outputLines = [];
36836
- worker.pendingFiles = [];
36837
- worker.startAt = void 0;
36838
- worker.remoteSocket = void 0;
36839
- worker.workerSocket = void 0;
36840
- }
36841
37081
  /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
36842
37082
  wireAssociationReceived(worker) {
36843
37083
  worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
36844
37084
  this.emit("ASSOCIATION_RECEIVED", {
36845
- associationId: worker.associationId ?? "",
37085
+ associationId: worker.context?.associationId ?? "",
36846
37086
  callingAE: data.callingAE,
36847
37087
  calledAE: data.calledAE,
36848
37088
  source: data.source
@@ -36853,7 +37093,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36853
37093
  wireCStoreRequest(worker) {
36854
37094
  worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
36855
37095
  this.emit("C_STORE_REQUEST", {
36856
- associationId: worker.associationId ?? "",
37096
+ associationId: worker.context?.associationId ?? "",
36857
37097
  raw: data.raw
36858
37098
  });
36859
37099
  });
@@ -36862,7 +37102,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36862
37102
  wireEchoRequest(worker) {
36863
37103
  worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
36864
37104
  this.emit("ECHO_REQUEST", {
36865
- associationId: worker.associationId ?? ""
37105
+ associationId: worker.context?.associationId ?? ""
36866
37106
  });
36867
37107
  });
36868
37108
  }
@@ -36877,8 +37117,8 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36877
37117
  /** Captures worker output lines during busy associations. */
36878
37118
  wireOutputCapture(worker) {
36879
37119
  worker.dcmrecv.on("line", ({ text }) => {
36880
- if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
36881
- worker.outputLines.push(text);
37120
+ if (worker.state === "busy") {
37121
+ worker.captureOutput(text);
36882
37122
  }
36883
37123
  });
36884
37124
  }
@@ -36897,60 +37137,6 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36897
37137
  signal.addEventListener("abort", this.abortHandler, { once: true });
36898
37138
  }
36899
37139
  };
36900
- async function ensureDirectory(dirPath) {
36901
- try {
36902
- await fs__namespace.mkdir(dirPath, { recursive: true });
36903
- return ok(void 0);
36904
- } catch (e) {
36905
- const msg = e instanceof Error ? e.message : String(e);
36906
- return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
36907
- }
36908
- }
36909
- async function moveFile(src, dest) {
36910
- try {
36911
- await fs__namespace.rename(src, dest);
36912
- return ok(void 0);
36913
- } catch {
36914
- try {
36915
- await fs__namespace.copyFile(src, dest);
36916
- await fs__namespace.unlink(src);
36917
- return ok(void 0);
36918
- } catch (e) {
36919
- const msg = e instanceof Error ? e.message : String(e);
36920
- return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
36921
- }
36922
- }
36923
- }
36924
- async function statFileSafe(filePath) {
36925
- try {
36926
- const stat4 = await fs__namespace.stat(filePath);
36927
- return stat4.size;
36928
- } catch {
36929
- return 0;
36930
- }
36931
- }
36932
- function sumArray(arr) {
36933
- let total = 0;
36934
- for (let i = 0; i < arr.length; i++) {
36935
- total += arr[i] ?? 0;
36936
- }
36937
- return total;
36938
- }
36939
- async function removeDirSafe(dirPath) {
36940
- try {
36941
- await fs__namespace.rm(dirPath, { recursive: true, force: true });
36942
- } catch {
36943
- }
36944
- }
36945
- function removePending(arr, promise) {
36946
- const idx = arr.indexOf(promise);
36947
- if (idx !== -1) void arr.splice(idx, 1);
36948
- }
36949
- function delay(ms) {
36950
- return new Promise((resolve) => {
36951
- setTimeout(resolve, ms);
36952
- });
36953
- }
36954
37140
 
36955
37141
  // src/senders/types.ts
36956
37142
  var SenderMode = {
@@ -37373,7 +37559,7 @@ function createStorescuExecutor(options) {
37373
37559
  return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37374
37560
  };
37375
37561
  }
37376
- function resolveConfig(options) {
37562
+ function resolveConfig2(options) {
37377
37563
  const mode = options.mode ?? "multiple";
37378
37564
  const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
37379
37565
  return {
@@ -37420,7 +37606,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37420
37606
  if (!validation.success) {
37421
37607
  return err(createValidationError("DicomSender", validation.error));
37422
37608
  }
37423
- const cfg = resolveConfig(options);
37609
+ const cfg = resolveConfig2(options);
37424
37610
  const senderRef = { current: void 0 };
37425
37611
  const engine = new SenderEngine({
37426
37612
  ...cfg,
@@ -37622,7 +37808,7 @@ function createDcmsendExecutor(options) {
37622
37808
  return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37623
37809
  };
37624
37810
  }
37625
- function resolveConfig2(options) {
37811
+ function resolveConfig3(options) {
37626
37812
  const mode = options.mode ?? "multiple";
37627
37813
  const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS2;
37628
37814
  return {
@@ -37669,7 +37855,7 @@ var DicomSend = class _DicomSend extends events.EventEmitter {
37669
37855
  if (!validation.success) {
37670
37856
  return err(createValidationError("DicomSend", validation.error));
37671
37857
  }
37672
- const cfg = resolveConfig2(options);
37858
+ const cfg = resolveConfig3(options);
37673
37859
  const senderRef = { current: void 0 };
37674
37860
  const engine = new SenderEngine({
37675
37861
  ...cfg,
@@ -37927,13 +38113,13 @@ function buildWorklistKeys(filter) {
37927
38113
  var MAX_RESPONSE_FILES = 1e4;
37928
38114
  async function createTempDir() {
37929
38115
  return stderrLib.tryCatch(
37930
- () => fs.mkdtemp(path.join(os.tmpdir(), "dcmtk-pacs-")),
38116
+ () => promises$1.mkdtemp(path.join(os.tmpdir(), "dcmtk-pacs-")),
37931
38117
  (e) => new Error(`Failed to create temp directory: ${e.message}`)
37932
38118
  );
37933
38119
  }
37934
38120
  async function listDcmFiles(directory) {
37935
38121
  try {
37936
- const entries = await fs.readdir(directory);
38122
+ const entries = await promises$1.readdir(directory);
37937
38123
  const dcmFiles = [];
37938
38124
  for (let i = 0; i < entries.length; i += 1) {
37939
38125
  const entry = entries[i];
@@ -37951,7 +38137,7 @@ async function listDcmFiles(directory) {
37951
38137
  }
37952
38138
  async function cleanupTempDir(directory) {
37953
38139
  try {
37954
- await fs.rm(directory, { recursive: true, force: true });
38140
+ await promises$1.rm(directory, { recursive: true, force: true });
37955
38141
  } catch {
37956
38142
  }
37957
38143
  }
@@ -38270,70 +38456,6 @@ var PacsClient = class _PacsClient {
38270
38456
  return ok({ success: true, outputDirectory: options.outputDirectory });
38271
38457
  }
38272
38458
  };
38273
- var MIN_CONCURRENCY = 1;
38274
- var MAX_CONCURRENCY = 64;
38275
- var DEFAULT_CONCURRENCY = 4;
38276
- function clampConcurrency(value) {
38277
- if (value === void 0) return DEFAULT_CONCURRENCY;
38278
- return Math.max(MIN_CONCURRENCY, Math.min(MAX_CONCURRENCY, Math.floor(value)));
38279
- }
38280
- function createBatchState(total) {
38281
- const results = [];
38282
- for (let i = 0; i < total; i += 1) {
38283
- results.push(void 0);
38284
- }
38285
- return { results, succeeded: 0, failed: 0, completed: 0 };
38286
- }
38287
- async function processItem(item, index, operation, state) {
38288
- let result;
38289
- try {
38290
- result = await operation(item);
38291
- } catch (thrown) {
38292
- result = err(stderrLib.stderr(thrown));
38293
- }
38294
- state.results[index] = result;
38295
- state.completed += 1;
38296
- if (result.ok) {
38297
- state.succeeded += 1;
38298
- } else {
38299
- state.failed += 1;
38300
- }
38301
- }
38302
- async function batch(items, operation, options) {
38303
- const concurrency = clampConcurrency(options?.concurrency);
38304
- const signal = options?.signal;
38305
- const onProgress = options?.onProgress;
38306
- const total = items.length;
38307
- if (total === 0) {
38308
- return { results: [], succeeded: 0, failed: 0 };
38309
- }
38310
- const state = createBatchState(total);
38311
- const inFlight = /* @__PURE__ */ new Set();
38312
- let nextIndex = 0;
38313
- while (nextIndex < total) {
38314
- if (signal?.aborted) break;
38315
- const itemIndex = nextIndex;
38316
- nextIndex += 1;
38317
- const promise = processItem(items[itemIndex], itemIndex, operation, state).then(() => {
38318
- if (onProgress) {
38319
- const result = state.results[itemIndex];
38320
- onProgress(state.completed, total, result);
38321
- }
38322
- });
38323
- inFlight.add(promise);
38324
- promise.then(
38325
- () => inFlight.delete(promise),
38326
- () => inFlight.delete(promise)
38327
- );
38328
- if (inFlight.size >= concurrency) {
38329
- await Promise.race(inFlight);
38330
- }
38331
- }
38332
- if (inFlight.size > 0) {
38333
- await Promise.all(inFlight);
38334
- }
38335
- return { results: state.results, succeeded: state.succeeded, failed: state.failed };
38336
- }
38337
38459
 
38338
38460
  // src/testing/types.ts
38339
38461
  var HammerPhase = {
@@ -38470,7 +38592,7 @@ var DicomHammer = class _DicomHammer extends events.EventEmitter {
38470
38592
  const start = Date.now();
38471
38593
  const dir = path.join(this.opts.outputDir, `dcmtk-hammer-${Date.now()}`);
38472
38594
  try {
38473
- await fs.mkdir(dir, { recursive: true });
38595
+ await promises$1.mkdir(dir, { recursive: true });
38474
38596
  } catch {
38475
38597
  return err(new Error(`Failed to create output directory: ${dir}`));
38476
38598
  }
@@ -38592,7 +38714,7 @@ var DicomHammer = class _DicomHammer extends events.EventEmitter {
38592
38714
  /** Gets the file size in bytes, returning 0 on error. */
38593
38715
  async getFileSize(filePath) {
38594
38716
  try {
38595
- const s = await fs.stat(filePath);
38717
+ const s = await promises$1.stat(filePath);
38596
38718
  return s.size;
38597
38719
  } catch {
38598
38720
  return 0;
@@ -38633,7 +38755,7 @@ var DicomHammer = class _DicomHammer extends events.EventEmitter {
38633
38755
  async cleanup() {
38634
38756
  if (this.generatedDir === void 0) return ok(void 0);
38635
38757
  try {
38636
- await fs.rm(this.generatedDir, { recursive: true, force: true });
38758
+ await promises$1.rm(this.generatedDir, { recursive: true, force: true });
38637
38759
  this.generatedDir = void 0;
38638
38760
  this.generatedFiles = [];
38639
38761
  return ok(void 0);
@@ -38670,80 +38792,6 @@ var DicomHammer = class _DicomHammer extends events.EventEmitter {
38670
38792
  }
38671
38793
  };
38672
38794
 
38673
- // src/utils/retry.ts
38674
- var DEFAULT_MAX_ATTEMPTS = 3;
38675
- var DEFAULT_INITIAL_DELAY_MS = 1e3;
38676
- var DEFAULT_MAX_DELAY_MS = 3e4;
38677
- var DEFAULT_BACKOFF_MULTIPLIER = 2;
38678
- var JITTER_FRACTION = 0.1;
38679
- var DEFAULT_CONFIG = {
38680
- maxAttempts: DEFAULT_MAX_ATTEMPTS,
38681
- initialDelayMs: DEFAULT_INITIAL_DELAY_MS,
38682
- maxDelayMs: DEFAULT_MAX_DELAY_MS,
38683
- backoffMultiplier: DEFAULT_BACKOFF_MULTIPLIER,
38684
- shouldRetry: void 0,
38685
- signal: void 0,
38686
- onRetry: void 0
38687
- };
38688
- function resolveConfig3(opts) {
38689
- if (!opts) return DEFAULT_CONFIG;
38690
- return {
38691
- ...DEFAULT_CONFIG,
38692
- ...Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0)),
38693
- maxAttempts: Math.max(1, Math.floor(opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)),
38694
- initialDelayMs: Math.max(0, opts.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS),
38695
- maxDelayMs: Math.max(0, opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS),
38696
- backoffMultiplier: Math.max(1, opts.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER)
38697
- };
38698
- }
38699
- function computeDelay(attempt, config) {
38700
- const baseDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
38701
- const jitter = baseDelay * JITTER_FRACTION * (2 * Math.random() - 1);
38702
- return Math.max(0, Math.round(baseDelay + jitter));
38703
- }
38704
- async function waitWithAbort(delayMs, signal) {
38705
- if (signal?.aborted) {
38706
- return false;
38707
- }
38708
- return new Promise((resolve) => {
38709
- const timer = setTimeout(() => {
38710
- cleanup();
38711
- resolve(true);
38712
- }, delayMs);
38713
- const onAbort = () => {
38714
- clearTimeout(timer);
38715
- cleanup();
38716
- resolve(false);
38717
- };
38718
- const cleanup = () => {
38719
- signal?.removeEventListener("abort", onAbort);
38720
- };
38721
- if (signal) {
38722
- signal.addEventListener("abort", onAbort, { once: true });
38723
- }
38724
- });
38725
- }
38726
- function shouldBreakAfterFailure(attempt, lastError, config) {
38727
- if (attempt === config.maxAttempts - 1) return true;
38728
- if (config.shouldRetry && !config.shouldRetry(lastError, attempt + 1)) return true;
38729
- if (config.signal?.aborted) return true;
38730
- return false;
38731
- }
38732
- async function retry(operation, options) {
38733
- const config = resolveConfig3(options);
38734
- let lastResult = err(new Error("No attempts executed"));
38735
- for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
38736
- lastResult = await operation();
38737
- if (lastResult.ok) return lastResult;
38738
- if (shouldBreakAfterFailure(attempt, lastResult.error, config)) break;
38739
- const delayMs = computeDelay(attempt, config);
38740
- if (config.onRetry) config.onRetry(lastResult.error, attempt + 1, delayMs);
38741
- const waitCompleted = await waitWithAbort(delayMs, config.signal);
38742
- if (!waitCompleted) break;
38743
- }
38744
- return lastResult;
38745
- }
38746
-
38747
38795
  exports.AETitleSchema = AETitleSchema;
38748
38796
  exports.AssociationTracker = AssociationTracker;
38749
38797
  exports.ChangeSet = ChangeSet;