@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/index.js CHANGED
@@ -6,12 +6,12 @@ import { spawn, execSync } from 'child_process';
6
6
  import kill from 'tree-kill';
7
7
  import { EventEmitter } from 'events';
8
8
  import { stderr, tryCatch } from 'stderr-lib';
9
- import * as fs from 'fs/promises';
10
- import { mkdir, stat, rm, copyFile, unlink, mkdtemp, readdir } from 'fs/promises';
9
+ import { mkdir, stat, rm, copyFile, unlink, rename, mkdtemp, readdir } from 'fs/promises';
11
10
  import * as os from 'os';
12
11
  import { tmpdir } from 'os';
13
12
  import { XMLParser } from 'fast-xml-parser';
14
13
  import * as net from 'net';
14
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
15
15
  import { randomUUID } from 'crypto';
16
16
 
17
17
  var __defProp = Object.defineProperty;
@@ -293,13 +293,14 @@ async function execCommand(binary, args, options) {
293
293
  windowsHide: true,
294
294
  signal: options?.signal
295
295
  });
296
- wireSpawnListeners(child, timeoutMs, resolve);
296
+ wireSpawnListeners(binary, child, timeoutMs, resolve);
297
297
  });
298
298
  }
299
- function wireSpawnListeners(child, timeoutMs, resolve) {
299
+ function wireSpawnListeners(binary, child, timeoutMs, resolve) {
300
300
  let stdout = "";
301
301
  let stderr8 = "";
302
302
  let settled = false;
303
+ const name = binary.split("/").pop() ?? binary;
303
304
  const settle = (result) => {
304
305
  if (settled) return;
305
306
  settled = true;
@@ -308,25 +309,25 @@ function wireSpawnListeners(child, timeoutMs, resolve) {
308
309
  };
309
310
  const timer = setTimeout(() => {
310
311
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
311
- settle(err(new Error(`Process timed out after ${timeoutMs}ms`)));
312
+ settle(err(new Error(`${name} timed out after ${timeoutMs}ms (pid: ${String(child.pid ?? "unknown")})`)));
312
313
  }, timeoutMs);
313
314
  child.stdout?.on("data", (chunk) => {
314
315
  stdout += String(chunk);
315
316
  if (stdout.length + stderr8.length > MAX_OUTPUT_BYTES) {
316
317
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
317
- settle(err(new Error(`Process output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
318
+ settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
318
319
  }
319
320
  });
320
321
  child.stderr?.on("data", (chunk) => {
321
322
  stderr8 += String(chunk);
322
323
  if (stdout.length + stderr8.length > MAX_OUTPUT_BYTES) {
323
324
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
324
- settle(err(new Error(`Process output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
325
+ settle(err(new Error(`${name} output exceeded ${MAX_OUTPUT_BYTES} bytes`)));
325
326
  }
326
327
  });
327
328
  child.on("error", (error) => {
328
329
  if (child.pid !== void 0 && child.pid !== null) killTree(child.pid);
329
- settle(err(new Error(`Process error: ${error.message}`)));
330
+ settle(err(new Error(`${name} error: ${error.message}`)));
330
331
  });
331
332
  child.on("close", (code) => {
332
333
  settle(ok({ stdout, stderr: stderr8, exitCode: code ?? 1 }));
@@ -341,7 +342,7 @@ async function spawnCommand(binary, args, options) {
341
342
  windowsHide: true,
342
343
  signal: options?.signal
343
344
  });
344
- wireSpawnListeners(child, timeoutMs, resolve);
345
+ wireSpawnListeners(binary, child, timeoutMs, resolve);
345
346
  });
346
347
  }
347
348
  var ProcessState = {
@@ -36247,6 +36248,184 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
36247
36248
  signal.addEventListener("abort", this.abortHandler, { once: true });
36248
36249
  }
36249
36250
  };
36251
+ var MIN_CONCURRENCY = 1;
36252
+ var MAX_CONCURRENCY = 64;
36253
+ var DEFAULT_CONCURRENCY = 4;
36254
+ function clampConcurrency(value) {
36255
+ if (value === void 0) return DEFAULT_CONCURRENCY;
36256
+ return Math.max(MIN_CONCURRENCY, Math.min(MAX_CONCURRENCY, Math.floor(value)));
36257
+ }
36258
+ function createBatchState(total) {
36259
+ const results = [];
36260
+ for (let i = 0; i < total; i += 1) {
36261
+ results.push(void 0);
36262
+ }
36263
+ return { results, succeeded: 0, failed: 0, completed: 0 };
36264
+ }
36265
+ async function processItem(item, index, operation, state) {
36266
+ let result;
36267
+ try {
36268
+ result = await operation(item);
36269
+ } catch (thrown) {
36270
+ result = err(stderr(thrown));
36271
+ }
36272
+ state.results[index] = result;
36273
+ state.completed += 1;
36274
+ if (result.ok) {
36275
+ state.succeeded += 1;
36276
+ } else {
36277
+ state.failed += 1;
36278
+ }
36279
+ }
36280
+ async function batch(items, operation, options) {
36281
+ const concurrency = clampConcurrency(options?.concurrency);
36282
+ const signal = options?.signal;
36283
+ const onProgress = options?.onProgress;
36284
+ const total = items.length;
36285
+ if (total === 0) {
36286
+ return { results: [], succeeded: 0, failed: 0 };
36287
+ }
36288
+ const state = createBatchState(total);
36289
+ const inFlight = /* @__PURE__ */ new Set();
36290
+ let nextIndex = 0;
36291
+ while (nextIndex < total) {
36292
+ if (signal?.aborted) break;
36293
+ const itemIndex = nextIndex;
36294
+ nextIndex += 1;
36295
+ const promise = processItem(items[itemIndex], itemIndex, operation, state).then(() => {
36296
+ if (onProgress) {
36297
+ const result = state.results[itemIndex];
36298
+ onProgress(state.completed, total, result);
36299
+ }
36300
+ });
36301
+ inFlight.add(promise);
36302
+ promise.then(
36303
+ () => inFlight.delete(promise),
36304
+ () => inFlight.delete(promise)
36305
+ );
36306
+ if (inFlight.size >= concurrency) {
36307
+ await Promise.race(inFlight);
36308
+ }
36309
+ }
36310
+ if (inFlight.size > 0) {
36311
+ await Promise.all(inFlight);
36312
+ }
36313
+ return { results: state.results, succeeded: state.succeeded, failed: state.failed };
36314
+ }
36315
+
36316
+ // src/utils/retry.ts
36317
+ var DEFAULT_MAX_ATTEMPTS = 3;
36318
+ var DEFAULT_INITIAL_DELAY_MS = 1e3;
36319
+ var DEFAULT_MAX_DELAY_MS = 3e4;
36320
+ var DEFAULT_BACKOFF_MULTIPLIER = 2;
36321
+ var JITTER_FRACTION = 0.1;
36322
+ var DEFAULT_CONFIG = {
36323
+ maxAttempts: DEFAULT_MAX_ATTEMPTS,
36324
+ initialDelayMs: DEFAULT_INITIAL_DELAY_MS,
36325
+ maxDelayMs: DEFAULT_MAX_DELAY_MS,
36326
+ backoffMultiplier: DEFAULT_BACKOFF_MULTIPLIER,
36327
+ shouldRetry: void 0,
36328
+ signal: void 0,
36329
+ onRetry: void 0
36330
+ };
36331
+ function resolveConfig(opts) {
36332
+ if (!opts) return DEFAULT_CONFIG;
36333
+ return {
36334
+ ...DEFAULT_CONFIG,
36335
+ ...Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0)),
36336
+ maxAttempts: Math.max(1, Math.floor(opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)),
36337
+ initialDelayMs: Math.max(0, opts.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS),
36338
+ maxDelayMs: Math.max(0, opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS),
36339
+ backoffMultiplier: Math.max(1, opts.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER)
36340
+ };
36341
+ }
36342
+ function computeDelay(attempt, config) {
36343
+ const baseDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
36344
+ const jitter = baseDelay * JITTER_FRACTION * (2 * Math.random() - 1);
36345
+ return Math.max(0, Math.round(baseDelay + jitter));
36346
+ }
36347
+ async function waitWithAbort(delayMs, signal) {
36348
+ if (signal?.aborted) {
36349
+ return false;
36350
+ }
36351
+ return new Promise((resolve) => {
36352
+ const timer = setTimeout(() => {
36353
+ cleanup();
36354
+ resolve(true);
36355
+ }, delayMs);
36356
+ const onAbort = () => {
36357
+ clearTimeout(timer);
36358
+ cleanup();
36359
+ resolve(false);
36360
+ };
36361
+ const cleanup = () => {
36362
+ signal?.removeEventListener("abort", onAbort);
36363
+ };
36364
+ if (signal) {
36365
+ signal.addEventListener("abort", onAbort, { once: true });
36366
+ }
36367
+ });
36368
+ }
36369
+ function shouldBreakAfterFailure(attempt, lastError, config) {
36370
+ if (attempt === config.maxAttempts - 1) return true;
36371
+ if (config.shouldRetry && !config.shouldRetry(lastError, attempt + 1)) return true;
36372
+ if (config.signal?.aborted) return true;
36373
+ return false;
36374
+ }
36375
+ async function retry(operation, options) {
36376
+ const config = resolveConfig(options);
36377
+ let lastResult = err(new Error("No attempts executed"));
36378
+ for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
36379
+ lastResult = await operation();
36380
+ if (lastResult.ok) return lastResult;
36381
+ if (shouldBreakAfterFailure(attempt, lastResult.error, config)) break;
36382
+ const delayMs = computeDelay(attempt, config);
36383
+ if (config.onRetry) config.onRetry(lastResult.error, attempt + 1, delayMs);
36384
+ const waitCompleted = await waitWithAbort(delayMs, config.signal);
36385
+ if (!waitCompleted) break;
36386
+ }
36387
+ return lastResult;
36388
+ }
36389
+ async function ensureDirectory(dirPath) {
36390
+ try {
36391
+ await mkdir(dirPath, { recursive: true });
36392
+ return ok(void 0);
36393
+ } catch (e) {
36394
+ const msg = e instanceof Error ? e.message : String(e);
36395
+ return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
36396
+ }
36397
+ }
36398
+ async function moveFile(src, dest) {
36399
+ try {
36400
+ await rename(src, dest);
36401
+ return ok(void 0);
36402
+ } catch {
36403
+ try {
36404
+ await copyFile(src, dest);
36405
+ await unlink(src);
36406
+ return ok(void 0);
36407
+ } catch (e) {
36408
+ const msg = e instanceof Error ? e.message : String(e);
36409
+ return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
36410
+ }
36411
+ }
36412
+ }
36413
+ async function statFileSafe(filePath) {
36414
+ try {
36415
+ const s = await stat(filePath);
36416
+ return s.size;
36417
+ } catch {
36418
+ return 0;
36419
+ }
36420
+ }
36421
+ async function removeDirSafe(dirPath) {
36422
+ try {
36423
+ await rm(dirPath, { recursive: true, force: true });
36424
+ } catch {
36425
+ }
36426
+ }
36427
+
36428
+ // src/servers/DicomReceiver.ts
36250
36429
  var DEFAULT_MIN_POOL_SIZE = 2;
36251
36430
  var DEFAULT_MAX_POOL_SIZE = 10;
36252
36431
  var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
@@ -36272,6 +36451,102 @@ var DicomReceiverOptionsSchema = z.object({
36272
36451
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
36273
36452
  message: "minPoolSize must be <= maxPoolSize"
36274
36453
  });
36454
+ var Worker = class {
36455
+ constructor(dcmrecv, port, tempDir) {
36456
+ __publicField(this, "dcmrecv");
36457
+ __publicField(this, "port");
36458
+ __publicField(this, "tempDir");
36459
+ __publicField(this, "_state", "idle");
36460
+ __publicField(this, "_context");
36461
+ __publicField(this, "_pending", /* @__PURE__ */ new Set());
36462
+ __publicField(this, "_files", []);
36463
+ __publicField(this, "_fileSizes", []);
36464
+ __publicField(this, "_outputLines", []);
36465
+ __publicField(this, "_remoteSocket");
36466
+ __publicField(this, "_workerSocket");
36467
+ this.dcmrecv = dcmrecv;
36468
+ this.port = port;
36469
+ this.tempDir = tempDir;
36470
+ }
36471
+ /** Current worker state. */
36472
+ get state() {
36473
+ return this._state;
36474
+ }
36475
+ /** Active association context, or undefined when idle. */
36476
+ get context() {
36477
+ return this._context;
36478
+ }
36479
+ /** Files moved to the association directory during the current association. */
36480
+ get files() {
36481
+ return this._files;
36482
+ }
36483
+ /** Byte sizes parallel to files[], for transfer stats. */
36484
+ get fileSizes() {
36485
+ return this._fileSizes;
36486
+ }
36487
+ /** Captured output lines during the current association. */
36488
+ get outputLines() {
36489
+ return this._outputLines;
36490
+ }
36491
+ /** Marks the worker busy with a new association context. */
36492
+ beginAssociation(ctx) {
36493
+ this._state = "busy";
36494
+ this._context = ctx;
36495
+ this._files.length = 0;
36496
+ this._fileSizes.length = 0;
36497
+ this._outputLines.length = 0;
36498
+ this._pending.clear();
36499
+ }
36500
+ /** Returns the worker to idle and clears all association state. */
36501
+ endAssociation() {
36502
+ this._state = "idle";
36503
+ this._context = void 0;
36504
+ this._files.length = 0;
36505
+ this._fileSizes.length = 0;
36506
+ this._outputLines.length = 0;
36507
+ this._pending.clear();
36508
+ this._remoteSocket = void 0;
36509
+ this._workerSocket = void 0;
36510
+ }
36511
+ /** Tracks an in-flight file handling promise; auto-removes on completion. */
36512
+ trackFile(promise) {
36513
+ this._pending.add(promise);
36514
+ void promise.finally(() => {
36515
+ this._pending.delete(promise);
36516
+ });
36517
+ }
36518
+ /** Records a successfully received file path and its size. */
36519
+ recordFile(filePath, size) {
36520
+ this._files.push(filePath);
36521
+ this._fileSizes.push(size);
36522
+ }
36523
+ /** Awaits all in-flight file handling promises. */
36524
+ async drainPendingFiles() {
36525
+ if (this._pending.size > 0) {
36526
+ await Promise.all(this._pending);
36527
+ }
36528
+ }
36529
+ /** Appends a line to the output buffer, respecting the cap. */
36530
+ captureOutput(text) {
36531
+ if (this._outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
36532
+ this._outputLines.push(text);
36533
+ }
36534
+ }
36535
+ /** Stores the remote and worker sockets for later cleanup. */
36536
+ setSockets(remote, worker) {
36537
+ this._remoteSocket = remote;
36538
+ this._workerSocket = worker;
36539
+ }
36540
+ /** Destroys both sockets if they exist and are not already destroyed. */
36541
+ destroySockets() {
36542
+ if (this._remoteSocket !== void 0 && !this._remoteSocket.destroyed) {
36543
+ this._remoteSocket.destroy();
36544
+ }
36545
+ if (this._workerSocket !== void 0 && !this._workerSocket.destroyed) {
36546
+ this._workerSocket.destroy();
36547
+ }
36548
+ }
36549
+ };
36275
36550
  function allocatePort() {
36276
36551
  return new Promise((resolve) => {
36277
36552
  const server = net.createServer();
@@ -36289,6 +36564,13 @@ function allocatePort() {
36289
36564
  });
36290
36565
  });
36291
36566
  }
36567
+ function sumArray(arr) {
36568
+ let total = 0;
36569
+ for (let i = 0; i < arr.length; i++) {
36570
+ total += arr[i] ?? 0;
36571
+ }
36572
+ return total;
36573
+ }
36292
36574
  var DicomReceiver = class _DicomReceiver extends EventEmitter {
36293
36575
  constructor(options) {
36294
36576
  super();
@@ -36303,8 +36585,6 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36303
36585
  __publicField(this, "stopping", false);
36304
36586
  __publicField(this, "abortHandler");
36305
36587
  this.setMaxListeners(20);
36306
- this.on("error", () => {
36307
- });
36308
36588
  this.options = options;
36309
36589
  this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
36310
36590
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
@@ -36512,14 +36792,12 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36512
36792
  this.emit("error", { error: mkdirResult.error });
36513
36793
  return;
36514
36794
  }
36515
- worker.state = "busy";
36516
- worker.associationId = associationId;
36517
- worker.associationDir = associationDir;
36518
- worker.files = [];
36519
- worker.fileSizes = [];
36520
- worker.outputLines = [];
36521
- worker.pendingFiles = [];
36522
- worker.startAt = Date.now();
36795
+ const ctx = {
36796
+ associationId,
36797
+ associationDir,
36798
+ startAt: Date.now()
36799
+ };
36800
+ worker.beginAssociation(ctx);
36523
36801
  this.pipeConnection(worker, remoteSocket);
36524
36802
  void this.replenishPool();
36525
36803
  }
@@ -36529,7 +36807,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36529
36807
  if (idle !== void 0) return idle;
36530
36808
  const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
36531
36809
  for (let i = 0; i < maxRetries; i++) {
36532
- await delay(CONNECTION_RETRY_INTERVAL_MS);
36810
+ await setTimeout$1(CONNECTION_RETRY_INTERVAL_MS);
36533
36811
  const found = this.getIdleWorker();
36534
36812
  if (found !== void 0) return found;
36535
36813
  if (this.stopping) return void 0;
@@ -36546,8 +36824,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36546
36824
  /** Pipes remote socket bidirectionally to the worker's port. */
36547
36825
  pipeConnection(worker, remoteSocket) {
36548
36826
  const workerSocket = net.createConnection({ port: worker.port, host: "127.0.0.1" });
36549
- worker.remoteSocket = remoteSocket;
36550
- worker.workerSocket = workerSocket;
36827
+ worker.setSockets(remoteSocket, workerSocket);
36551
36828
  const cleanup = () => {
36552
36829
  remoteSocket.unpipe(workerSocket);
36553
36830
  workerSocket.unpipe(remoteSocket);
@@ -36606,21 +36883,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36606
36883
  const createResult = this.createDcmrecv(port, tempDir);
36607
36884
  if (!createResult.ok) return createResult;
36608
36885
  const dcmrecv = createResult.value;
36609
- const worker = {
36610
- dcmrecv,
36611
- port,
36612
- tempDir,
36613
- state: "idle",
36614
- associationId: void 0,
36615
- associationDir: void 0,
36616
- files: [],
36617
- fileSizes: [],
36618
- outputLines: [],
36619
- pendingFiles: [],
36620
- startAt: void 0,
36621
- remoteSocket: void 0,
36622
- workerSocket: void 0
36623
- };
36886
+ const worker = new Worker(dcmrecv, port, tempDir);
36624
36887
  this.wireWorkerEvents(worker);
36625
36888
  const startResult = await dcmrecv.start();
36626
36889
  if (!startResult.ok) {
@@ -36629,14 +36892,9 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36629
36892
  this.workers.set(port, worker);
36630
36893
  return ok(worker);
36631
36894
  }
36632
- /** Stops a single worker: stop process, clean temp dir, remove from pool. */
36895
+ /** Stops a single worker: destroy sockets, stop process, clean temp dir, remove from pool. */
36633
36896
  async stopWorker(worker) {
36634
- if (worker.remoteSocket !== void 0 && !worker.remoteSocket.destroyed) {
36635
- worker.remoteSocket.destroy();
36636
- }
36637
- if (worker.workerSocket !== void 0 && !worker.workerSocket.destroyed) {
36638
- worker.workerSocket.destroy();
36639
- }
36897
+ worker.destroySockets();
36640
36898
  await worker.dcmrecv.stop();
36641
36899
  worker.dcmrecv[Symbol.dispose]();
36642
36900
  await removeDirSafe(worker.tempDir);
@@ -36688,12 +36946,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36688
36946
  this.wireRefusingAssociation(worker);
36689
36947
  this.wireOutputCapture(worker);
36690
36948
  }
36691
- /** Wires FILE_RECEIVED from dcmrecv worker — files are processed serially per worker. */
36949
+ /** Wires FILE_RECEIVED from dcmrecv worker — captures context synchronously. */
36692
36950
  wireFileReceived(worker) {
36693
36951
  worker.dcmrecv.onFileReceived((data) => {
36694
- const assocId = worker.associationId;
36695
- const assocDir = worker.associationDir;
36696
- if (assocDir === void 0 || assocId === void 0) {
36952
+ const ctx = worker.context;
36953
+ if (ctx === void 0) {
36697
36954
  this.emit("error", {
36698
36955
  error: new Error(`DicomReceiver: FILE_RECEIVED with no active association (worker state: ${worker.state})`),
36699
36956
  filePath: data.filePath,
@@ -36703,21 +36960,21 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36703
36960
  });
36704
36961
  return;
36705
36962
  }
36706
- const promise = this.handleFileReceived(worker, data, assocId, assocDir);
36707
- worker.pendingFiles.push(promise);
36963
+ const promise = this.handleFileReceived(worker, data, ctx);
36964
+ worker.trackFile(promise);
36708
36965
  });
36709
36966
  }
36710
36967
  /** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
36711
- async handleFileReceived(worker, data, assocId, assocDir) {
36968
+ async handleFileReceived(worker, data, ctx) {
36712
36969
  try {
36713
- await this.moveAndEmitFile(worker, data, assocId, assocDir);
36970
+ await this.moveAndEmitFile(worker, data, ctx);
36714
36971
  } catch (thrown) {
36715
36972
  const error = thrown instanceof Error ? thrown : new Error(String(thrown));
36716
36973
  this.emit("error", {
36717
36974
  error,
36718
36975
  filePath: data.filePath,
36719
- associationId: assocId,
36720
- associationDir: assocDir,
36976
+ associationId: ctx.associationId,
36977
+ associationDir: ctx.associationDir,
36721
36978
  callingAE: data.callingAE,
36722
36979
  calledAE: data.calledAE,
36723
36980
  source: data.source
@@ -36725,21 +36982,20 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36725
36982
  }
36726
36983
  }
36727
36984
  /** Inner handler: move file, parse DICOM, emit FILE_RECEIVED or error. */
36728
- async moveAndEmitFile(worker, data, assocId, assocDir) {
36985
+ async moveAndEmitFile(worker, data, ctx) {
36729
36986
  const srcPath = data.filePath;
36730
- const destPath = path.join(assocDir, path.basename(srcPath));
36987
+ const destPath = path.join(ctx.associationDir, path.basename(srcPath));
36731
36988
  const moveResult = await moveFile(srcPath, destPath);
36732
36989
  const finalPath = moveResult.ok ? destPath : srcPath;
36733
36990
  const fileSize = await statFileSafe(finalPath);
36734
- worker.files.push(finalPath);
36735
- worker.fileSizes.push(fileSize);
36991
+ worker.recordFile(finalPath, fileSize);
36736
36992
  const openResult = await DicomInstance.open(finalPath);
36737
36993
  if (!openResult.ok) {
36738
36994
  this.emit("error", {
36739
36995
  error: openResult.error,
36740
36996
  filePath: finalPath,
36741
- associationId: assocId,
36742
- associationDir: assocDir,
36997
+ associationId: ctx.associationId,
36998
+ associationDir: ctx.associationDir,
36743
36999
  callingAE: data.callingAE,
36744
37000
  calledAE: data.calledAE,
36745
37001
  source: data.source
@@ -36749,8 +37005,8 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36749
37005
  this.emit("FILE_RECEIVED", {
36750
37006
  filePath: finalPath,
36751
37007
  fileSize,
36752
- associationId: assocId,
36753
- associationDir: assocDir,
37008
+ associationId: ctx.associationId,
37009
+ associationDir: ctx.associationDir,
36754
37010
  callingAE: data.callingAE,
36755
37011
  calledAE: data.calledAE,
36756
37012
  source: data.source,
@@ -36765,21 +37021,20 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36765
37021
  }
36766
37022
  /** Awaits ALL pending file operations, emits ASSOCIATION_COMPLETE, resets worker state. */
36767
37023
  async finalizeAssociation(worker, data) {
36768
- if (worker.pendingFiles.length > 0) {
36769
- await Promise.all(worker.pendingFiles);
36770
- }
37024
+ await worker.drainPendingFiles();
36771
37025
  this.emitAssociationComplete(worker, data);
36772
- this.resetWorker(worker);
37026
+ worker.endAssociation();
36773
37027
  void this.scaleDown();
36774
37028
  }
36775
37029
  /** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
36776
37030
  emitAssociationComplete(worker, data) {
36777
- const assocId = worker.associationId ?? data.associationId;
36778
- const assocDir = worker.associationDir ?? "";
37031
+ const ctx = worker.context;
37032
+ const assocId = ctx?.associationId ?? data.associationId;
37033
+ const assocDir = ctx?.associationDir ?? "";
37034
+ const startAt = ctx?.startAt ?? Date.now();
36779
37035
  const files = [...worker.files];
36780
37036
  const output = [...worker.outputLines];
36781
37037
  const endAt = Date.now();
36782
- const startAt = worker.startAt ?? endAt;
36783
37038
  const totalBytes = sumArray(worker.fileSizes);
36784
37039
  const elapsedMs = endAt - startAt;
36785
37040
  const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
@@ -36799,24 +37054,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36799
37054
  output
36800
37055
  });
36801
37056
  }
36802
- /** Resets worker state to idle after an association completes. */
36803
- resetWorker(worker) {
36804
- worker.state = "idle";
36805
- worker.associationId = void 0;
36806
- worker.associationDir = void 0;
36807
- worker.files = [];
36808
- worker.fileSizes = [];
36809
- worker.outputLines = [];
36810
- worker.pendingFiles = [];
36811
- worker.startAt = void 0;
36812
- worker.remoteSocket = void 0;
36813
- worker.workerSocket = void 0;
36814
- }
36815
37057
  /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
36816
37058
  wireAssociationReceived(worker) {
36817
37059
  worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
36818
37060
  this.emit("ASSOCIATION_RECEIVED", {
36819
- associationId: worker.associationId ?? "",
37061
+ associationId: worker.context?.associationId ?? "",
36820
37062
  callingAE: data.callingAE,
36821
37063
  calledAE: data.calledAE,
36822
37064
  source: data.source
@@ -36827,7 +37069,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36827
37069
  wireCStoreRequest(worker) {
36828
37070
  worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
36829
37071
  this.emit("C_STORE_REQUEST", {
36830
- associationId: worker.associationId ?? "",
37072
+ associationId: worker.context?.associationId ?? "",
36831
37073
  raw: data.raw
36832
37074
  });
36833
37075
  });
@@ -36836,7 +37078,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36836
37078
  wireEchoRequest(worker) {
36837
37079
  worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
36838
37080
  this.emit("ECHO_REQUEST", {
36839
- associationId: worker.associationId ?? ""
37081
+ associationId: worker.context?.associationId ?? ""
36840
37082
  });
36841
37083
  });
36842
37084
  }
@@ -36851,8 +37093,8 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36851
37093
  /** Captures worker output lines during busy associations. */
36852
37094
  wireOutputCapture(worker) {
36853
37095
  worker.dcmrecv.on("line", ({ text }) => {
36854
- if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
36855
- worker.outputLines.push(text);
37096
+ if (worker.state === "busy") {
37097
+ worker.captureOutput(text);
36856
37098
  }
36857
37099
  });
36858
37100
  }
@@ -36871,56 +37113,6 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36871
37113
  signal.addEventListener("abort", this.abortHandler, { once: true });
36872
37114
  }
36873
37115
  };
36874
- async function ensureDirectory(dirPath) {
36875
- try {
36876
- await fs.mkdir(dirPath, { recursive: true });
36877
- return ok(void 0);
36878
- } catch (e) {
36879
- const msg = e instanceof Error ? e.message : String(e);
36880
- return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
36881
- }
36882
- }
36883
- async function moveFile(src, dest) {
36884
- try {
36885
- await fs.rename(src, dest);
36886
- return ok(void 0);
36887
- } catch {
36888
- try {
36889
- await fs.copyFile(src, dest);
36890
- await fs.unlink(src);
36891
- return ok(void 0);
36892
- } catch (e) {
36893
- const msg = e instanceof Error ? e.message : String(e);
36894
- return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
36895
- }
36896
- }
36897
- }
36898
- async function statFileSafe(filePath) {
36899
- try {
36900
- const stat4 = await fs.stat(filePath);
36901
- return stat4.size;
36902
- } catch {
36903
- return 0;
36904
- }
36905
- }
36906
- function sumArray(arr) {
36907
- let total = 0;
36908
- for (let i = 0; i < arr.length; i++) {
36909
- total += arr[i] ?? 0;
36910
- }
36911
- return total;
36912
- }
36913
- async function removeDirSafe(dirPath) {
36914
- try {
36915
- await fs.rm(dirPath, { recursive: true, force: true });
36916
- } catch {
36917
- }
36918
- }
36919
- function delay(ms) {
36920
- return new Promise((resolve) => {
36921
- setTimeout(resolve, ms);
36922
- });
36923
- }
36924
37116
 
36925
37117
  // src/senders/types.ts
36926
37118
  var SenderMode = {
@@ -37343,7 +37535,7 @@ function createStorescuExecutor(options) {
37343
37535
  return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37344
37536
  };
37345
37537
  }
37346
- function resolveConfig(options) {
37538
+ function resolveConfig2(options) {
37347
37539
  const mode = options.mode ?? "multiple";
37348
37540
  const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
37349
37541
  return {
@@ -37390,7 +37582,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37390
37582
  if (!validation.success) {
37391
37583
  return err(createValidationError("DicomSender", validation.error));
37392
37584
  }
37393
- const cfg = resolveConfig(options);
37585
+ const cfg = resolveConfig2(options);
37394
37586
  const senderRef = { current: void 0 };
37395
37587
  const engine = new SenderEngine({
37396
37588
  ...cfg,
@@ -37592,7 +37784,7 @@ function createDcmsendExecutor(options) {
37592
37784
  return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37593
37785
  };
37594
37786
  }
37595
- function resolveConfig2(options) {
37787
+ function resolveConfig3(options) {
37596
37788
  const mode = options.mode ?? "multiple";
37597
37789
  const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS2;
37598
37790
  return {
@@ -37639,7 +37831,7 @@ var DicomSend = class _DicomSend extends EventEmitter {
37639
37831
  if (!validation.success) {
37640
37832
  return err(createValidationError("DicomSend", validation.error));
37641
37833
  }
37642
- const cfg = resolveConfig2(options);
37834
+ const cfg = resolveConfig3(options);
37643
37835
  const senderRef = { current: void 0 };
37644
37836
  const engine = new SenderEngine({
37645
37837
  ...cfg,
@@ -38240,70 +38432,6 @@ var PacsClient = class _PacsClient {
38240
38432
  return ok({ success: true, outputDirectory: options.outputDirectory });
38241
38433
  }
38242
38434
  };
38243
- var MIN_CONCURRENCY = 1;
38244
- var MAX_CONCURRENCY = 64;
38245
- var DEFAULT_CONCURRENCY = 4;
38246
- function clampConcurrency(value) {
38247
- if (value === void 0) return DEFAULT_CONCURRENCY;
38248
- return Math.max(MIN_CONCURRENCY, Math.min(MAX_CONCURRENCY, Math.floor(value)));
38249
- }
38250
- function createBatchState(total) {
38251
- const results = [];
38252
- for (let i = 0; i < total; i += 1) {
38253
- results.push(void 0);
38254
- }
38255
- return { results, succeeded: 0, failed: 0, completed: 0 };
38256
- }
38257
- async function processItem(item, index, operation, state) {
38258
- let result;
38259
- try {
38260
- result = await operation(item);
38261
- } catch (thrown) {
38262
- result = err(stderr(thrown));
38263
- }
38264
- state.results[index] = result;
38265
- state.completed += 1;
38266
- if (result.ok) {
38267
- state.succeeded += 1;
38268
- } else {
38269
- state.failed += 1;
38270
- }
38271
- }
38272
- async function batch(items, operation, options) {
38273
- const concurrency = clampConcurrency(options?.concurrency);
38274
- const signal = options?.signal;
38275
- const onProgress = options?.onProgress;
38276
- const total = items.length;
38277
- if (total === 0) {
38278
- return { results: [], succeeded: 0, failed: 0 };
38279
- }
38280
- const state = createBatchState(total);
38281
- const inFlight = /* @__PURE__ */ new Set();
38282
- let nextIndex = 0;
38283
- while (nextIndex < total) {
38284
- if (signal?.aborted) break;
38285
- const itemIndex = nextIndex;
38286
- nextIndex += 1;
38287
- const promise = processItem(items[itemIndex], itemIndex, operation, state).then(() => {
38288
- if (onProgress) {
38289
- const result = state.results[itemIndex];
38290
- onProgress(state.completed, total, result);
38291
- }
38292
- });
38293
- inFlight.add(promise);
38294
- promise.then(
38295
- () => inFlight.delete(promise),
38296
- () => inFlight.delete(promise)
38297
- );
38298
- if (inFlight.size >= concurrency) {
38299
- await Promise.race(inFlight);
38300
- }
38301
- }
38302
- if (inFlight.size > 0) {
38303
- await Promise.all(inFlight);
38304
- }
38305
- return { results: state.results, succeeded: state.succeeded, failed: state.failed };
38306
- }
38307
38435
 
38308
38436
  // src/testing/types.ts
38309
38437
  var HammerPhase = {
@@ -38640,80 +38768,6 @@ var DicomHammer = class _DicomHammer extends EventEmitter {
38640
38768
  }
38641
38769
  };
38642
38770
 
38643
- // src/utils/retry.ts
38644
- var DEFAULT_MAX_ATTEMPTS = 3;
38645
- var DEFAULT_INITIAL_DELAY_MS = 1e3;
38646
- var DEFAULT_MAX_DELAY_MS = 3e4;
38647
- var DEFAULT_BACKOFF_MULTIPLIER = 2;
38648
- var JITTER_FRACTION = 0.1;
38649
- var DEFAULT_CONFIG = {
38650
- maxAttempts: DEFAULT_MAX_ATTEMPTS,
38651
- initialDelayMs: DEFAULT_INITIAL_DELAY_MS,
38652
- maxDelayMs: DEFAULT_MAX_DELAY_MS,
38653
- backoffMultiplier: DEFAULT_BACKOFF_MULTIPLIER,
38654
- shouldRetry: void 0,
38655
- signal: void 0,
38656
- onRetry: void 0
38657
- };
38658
- function resolveConfig3(opts) {
38659
- if (!opts) return DEFAULT_CONFIG;
38660
- return {
38661
- ...DEFAULT_CONFIG,
38662
- ...Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0)),
38663
- maxAttempts: Math.max(1, Math.floor(opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)),
38664
- initialDelayMs: Math.max(0, opts.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS),
38665
- maxDelayMs: Math.max(0, opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS),
38666
- backoffMultiplier: Math.max(1, opts.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER)
38667
- };
38668
- }
38669
- function computeDelay(attempt, config) {
38670
- const baseDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
38671
- const jitter = baseDelay * JITTER_FRACTION * (2 * Math.random() - 1);
38672
- return Math.max(0, Math.round(baseDelay + jitter));
38673
- }
38674
- async function waitWithAbort(delayMs, signal) {
38675
- if (signal?.aborted) {
38676
- return false;
38677
- }
38678
- return new Promise((resolve) => {
38679
- const timer = setTimeout(() => {
38680
- cleanup();
38681
- resolve(true);
38682
- }, delayMs);
38683
- const onAbort = () => {
38684
- clearTimeout(timer);
38685
- cleanup();
38686
- resolve(false);
38687
- };
38688
- const cleanup = () => {
38689
- signal?.removeEventListener("abort", onAbort);
38690
- };
38691
- if (signal) {
38692
- signal.addEventListener("abort", onAbort, { once: true });
38693
- }
38694
- });
38695
- }
38696
- function shouldBreakAfterFailure(attempt, lastError, config) {
38697
- if (attempt === config.maxAttempts - 1) return true;
38698
- if (config.shouldRetry && !config.shouldRetry(lastError, attempt + 1)) return true;
38699
- if (config.signal?.aborted) return true;
38700
- return false;
38701
- }
38702
- async function retry(operation, options) {
38703
- const config = resolveConfig3(options);
38704
- let lastResult = err(new Error("No attempts executed"));
38705
- for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
38706
- lastResult = await operation();
38707
- if (lastResult.ok) return lastResult;
38708
- if (shouldBreakAfterFailure(attempt, lastResult.error, config)) break;
38709
- const delayMs = computeDelay(attempt, config);
38710
- if (config.onRetry) config.onRetry(lastResult.error, attempt + 1, delayMs);
38711
- const waitCompleted = await waitWithAbort(delayMs, config.signal);
38712
- if (!waitCompleted) break;
38713
- }
38714
- return lastResult;
38715
- }
38716
-
38717
38771
  export { AETitleSchema, AssociationTracker, ChangeSet, ColorConversion, DCMPRSCP_FATAL_EVENTS, DCMPRSCP_PATTERNS, DCMPSRCV_FATAL_EVENTS, DCMPSRCV_PATTERNS, DCMQRSCP_FATAL_EVENTS, DCMQRSCP_PATTERNS, DCMRECV_FATAL_EVENTS, DCMRECV_PATTERNS, DEFAULT_BLOCK_TIMEOUT_MS, DEFAULT_DICOM_PORT, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PARSE_CONCURRENCY, DEFAULT_START_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, Dcm2pnmOutputFormat, Dcm2xmlCharset, DcmQRSCP, DcmdumpFormat, Dcmj2pnmOutputFormat, DcmprsCP, DcmprscpEvent, Dcmpsrcv, DcmpsrcvEvent, DcmqrscpEvent, Dcmrecv, DcmrecvEvent, DcmtkProcess, DicomDataset, DicomHammer, DicomInstance, DicomReceiver, DicomSend, DicomSender, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, HammerPhase, Img2dcmInputFormat, JplsColorConversion, LineParser, LutType, MAX_BLOCK_LINES, MAX_CHANGESET_OPERATIONS, MAX_EVENT_PATTERNS, MAX_TRAVERSAL_DEPTH, MoveQueryModel, PDU_SIZE, PacsClient, PortSchema, PreferredTransferSyntax, ProcessState, ProposedTransferSyntax, QueryLevel, QueryModel, REQUIRED_BINARIES, RetrieveMode, SOP_CLASSES, STORESCP_FATAL_EVENTS, STORESCP_PATTERNS, SenderHealth, SenderMode, StorageMode, StoreSCP, StoreSCPPreset, StorescpEvent, SubdirectoryMode, TransferSyntax, UIDSchema, UNIX_SEARCH_PATHS, VR, VR_CATEGORY, VR_CATEGORY_NAME, VR_META, WINDOWS_SEARCH_PATHS, WLMSCPFS_FATAL_EVENTS, WLMSCPFS_PATTERNS, Wlmscpfs, WlmscpfsEvent, assertUnreachable, batch, cda2dcm, clearDcmtkPathCache, createAETitle, createDicomFilePath, createDicomTag, createDicomTagPath, createPort, createSOPClassUID, createTransferSyntaxUID, dcm2cda, dcm2json, dcm2pdf, dcm2pnm, dcm2xml, dcmcjpeg, dcmcjpls, dcmconv, dcmcrle, dcmdecap, dcmdjpeg, dcmdjpls, dcmdrle, dcmdspfn, dcmdump, dcmencap, dcmftest, dcmgpdir, dcmj2pnm, dcmmkcrv, dcmmkdir, dcmmklut, dcmodify, dcmp2pgm, dcmprscu, dcmpschk, dcmpsmk, dcmpsprt, dcmqridx, dcmquant, dcmscale, dcmsend, dcod2lum, dconvlum, drtdump, dsr2xml, dsrdump, dump2dcm, echoscu, err, execCommand, findDcmtkPath, findscu, getVRCategory, getscu, img2dcm, isBinaryVR, isNumericVR, isStringVR, json2dcm, lookupTag, lookupTagByKeyword, lookupTagByName, mapResult, movescu, ok, parseAETitle, parseDicomTag, parseDicomTagPath, parsePort, parseSOPClassUID, parseTransferSyntaxUID, pdf2dcm, retry, segmentsToModifyPath, segmentsToString, sopClassNameFromUID, spawnCommand, stl2dcm, storescu, tag, tagPathToSegments, termscu, walkTags, xml2dcm, xml2dsr, xmlToJson };
38718
38772
  //# sourceMappingURL=index.js.map
38719
38773
  //# sourceMappingURL=index.js.map