@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.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;
@@ -36247,6 +36247,184 @@ var Wlmscpfs = class _Wlmscpfs extends DcmtkProcess {
36247
36247
  signal.addEventListener("abort", this.abortHandler, { once: true });
36248
36248
  }
36249
36249
  };
36250
+ var MIN_CONCURRENCY = 1;
36251
+ var MAX_CONCURRENCY = 64;
36252
+ var DEFAULT_CONCURRENCY = 4;
36253
+ function clampConcurrency(value) {
36254
+ if (value === void 0) return DEFAULT_CONCURRENCY;
36255
+ return Math.max(MIN_CONCURRENCY, Math.min(MAX_CONCURRENCY, Math.floor(value)));
36256
+ }
36257
+ function createBatchState(total) {
36258
+ const results = [];
36259
+ for (let i = 0; i < total; i += 1) {
36260
+ results.push(void 0);
36261
+ }
36262
+ return { results, succeeded: 0, failed: 0, completed: 0 };
36263
+ }
36264
+ async function processItem(item, index, operation, state) {
36265
+ let result;
36266
+ try {
36267
+ result = await operation(item);
36268
+ } catch (thrown) {
36269
+ result = err(stderr(thrown));
36270
+ }
36271
+ state.results[index] = result;
36272
+ state.completed += 1;
36273
+ if (result.ok) {
36274
+ state.succeeded += 1;
36275
+ } else {
36276
+ state.failed += 1;
36277
+ }
36278
+ }
36279
+ async function batch(items, operation, options) {
36280
+ const concurrency = clampConcurrency(options?.concurrency);
36281
+ const signal = options?.signal;
36282
+ const onProgress = options?.onProgress;
36283
+ const total = items.length;
36284
+ if (total === 0) {
36285
+ return { results: [], succeeded: 0, failed: 0 };
36286
+ }
36287
+ const state = createBatchState(total);
36288
+ const inFlight = /* @__PURE__ */ new Set();
36289
+ let nextIndex = 0;
36290
+ while (nextIndex < total) {
36291
+ if (signal?.aborted) break;
36292
+ const itemIndex = nextIndex;
36293
+ nextIndex += 1;
36294
+ const promise = processItem(items[itemIndex], itemIndex, operation, state).then(() => {
36295
+ if (onProgress) {
36296
+ const result = state.results[itemIndex];
36297
+ onProgress(state.completed, total, result);
36298
+ }
36299
+ });
36300
+ inFlight.add(promise);
36301
+ promise.then(
36302
+ () => inFlight.delete(promise),
36303
+ () => inFlight.delete(promise)
36304
+ );
36305
+ if (inFlight.size >= concurrency) {
36306
+ await Promise.race(inFlight);
36307
+ }
36308
+ }
36309
+ if (inFlight.size > 0) {
36310
+ await Promise.all(inFlight);
36311
+ }
36312
+ return { results: state.results, succeeded: state.succeeded, failed: state.failed };
36313
+ }
36314
+
36315
+ // src/utils/retry.ts
36316
+ var DEFAULT_MAX_ATTEMPTS = 3;
36317
+ var DEFAULT_INITIAL_DELAY_MS = 1e3;
36318
+ var DEFAULT_MAX_DELAY_MS = 3e4;
36319
+ var DEFAULT_BACKOFF_MULTIPLIER = 2;
36320
+ var JITTER_FRACTION = 0.1;
36321
+ var DEFAULT_CONFIG = {
36322
+ maxAttempts: DEFAULT_MAX_ATTEMPTS,
36323
+ initialDelayMs: DEFAULT_INITIAL_DELAY_MS,
36324
+ maxDelayMs: DEFAULT_MAX_DELAY_MS,
36325
+ backoffMultiplier: DEFAULT_BACKOFF_MULTIPLIER,
36326
+ shouldRetry: void 0,
36327
+ signal: void 0,
36328
+ onRetry: void 0
36329
+ };
36330
+ function resolveConfig(opts) {
36331
+ if (!opts) return DEFAULT_CONFIG;
36332
+ return {
36333
+ ...DEFAULT_CONFIG,
36334
+ ...Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0)),
36335
+ maxAttempts: Math.max(1, Math.floor(opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)),
36336
+ initialDelayMs: Math.max(0, opts.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS),
36337
+ maxDelayMs: Math.max(0, opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS),
36338
+ backoffMultiplier: Math.max(1, opts.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER)
36339
+ };
36340
+ }
36341
+ function computeDelay(attempt, config) {
36342
+ const baseDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
36343
+ const jitter = baseDelay * JITTER_FRACTION * (2 * Math.random() - 1);
36344
+ return Math.max(0, Math.round(baseDelay + jitter));
36345
+ }
36346
+ async function waitWithAbort(delayMs, signal) {
36347
+ if (signal?.aborted) {
36348
+ return false;
36349
+ }
36350
+ return new Promise((resolve) => {
36351
+ const timer = setTimeout(() => {
36352
+ cleanup();
36353
+ resolve(true);
36354
+ }, delayMs);
36355
+ const onAbort = () => {
36356
+ clearTimeout(timer);
36357
+ cleanup();
36358
+ resolve(false);
36359
+ };
36360
+ const cleanup = () => {
36361
+ signal?.removeEventListener("abort", onAbort);
36362
+ };
36363
+ if (signal) {
36364
+ signal.addEventListener("abort", onAbort, { once: true });
36365
+ }
36366
+ });
36367
+ }
36368
+ function shouldBreakAfterFailure(attempt, lastError, config) {
36369
+ if (attempt === config.maxAttempts - 1) return true;
36370
+ if (config.shouldRetry && !config.shouldRetry(lastError, attempt + 1)) return true;
36371
+ if (config.signal?.aborted) return true;
36372
+ return false;
36373
+ }
36374
+ async function retry(operation, options) {
36375
+ const config = resolveConfig(options);
36376
+ let lastResult = err(new Error("No attempts executed"));
36377
+ for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
36378
+ lastResult = await operation();
36379
+ if (lastResult.ok) return lastResult;
36380
+ if (shouldBreakAfterFailure(attempt, lastResult.error, config)) break;
36381
+ const delayMs = computeDelay(attempt, config);
36382
+ if (config.onRetry) config.onRetry(lastResult.error, attempt + 1, delayMs);
36383
+ const waitCompleted = await waitWithAbort(delayMs, config.signal);
36384
+ if (!waitCompleted) break;
36385
+ }
36386
+ return lastResult;
36387
+ }
36388
+ async function ensureDirectory(dirPath) {
36389
+ try {
36390
+ await mkdir(dirPath, { recursive: true });
36391
+ return ok(void 0);
36392
+ } catch (e) {
36393
+ const msg = e instanceof Error ? e.message : String(e);
36394
+ return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
36395
+ }
36396
+ }
36397
+ async function moveFile(src, dest) {
36398
+ try {
36399
+ await rename(src, dest);
36400
+ return ok(void 0);
36401
+ } catch {
36402
+ try {
36403
+ await copyFile(src, dest);
36404
+ await unlink(src);
36405
+ return ok(void 0);
36406
+ } catch (e) {
36407
+ const msg = e instanceof Error ? e.message : String(e);
36408
+ return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
36409
+ }
36410
+ }
36411
+ }
36412
+ async function statFileSafe(filePath) {
36413
+ try {
36414
+ const s = await stat(filePath);
36415
+ return s.size;
36416
+ } catch {
36417
+ return 0;
36418
+ }
36419
+ }
36420
+ async function removeDirSafe(dirPath) {
36421
+ try {
36422
+ await rm(dirPath, { recursive: true, force: true });
36423
+ } catch {
36424
+ }
36425
+ }
36426
+
36427
+ // src/servers/DicomReceiver.ts
36250
36428
  var DEFAULT_MIN_POOL_SIZE = 2;
36251
36429
  var DEFAULT_MAX_POOL_SIZE = 10;
36252
36430
  var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
@@ -36272,6 +36450,102 @@ var DicomReceiverOptionsSchema = z.object({
36272
36450
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
36273
36451
  message: "minPoolSize must be <= maxPoolSize"
36274
36452
  });
36453
+ var Worker = class {
36454
+ constructor(dcmrecv, port, tempDir) {
36455
+ __publicField(this, "dcmrecv");
36456
+ __publicField(this, "port");
36457
+ __publicField(this, "tempDir");
36458
+ __publicField(this, "_state", "idle");
36459
+ __publicField(this, "_context");
36460
+ __publicField(this, "_pending", /* @__PURE__ */ new Set());
36461
+ __publicField(this, "_files", []);
36462
+ __publicField(this, "_fileSizes", []);
36463
+ __publicField(this, "_outputLines", []);
36464
+ __publicField(this, "_remoteSocket");
36465
+ __publicField(this, "_workerSocket");
36466
+ this.dcmrecv = dcmrecv;
36467
+ this.port = port;
36468
+ this.tempDir = tempDir;
36469
+ }
36470
+ /** Current worker state. */
36471
+ get state() {
36472
+ return this._state;
36473
+ }
36474
+ /** Active association context, or undefined when idle. */
36475
+ get context() {
36476
+ return this._context;
36477
+ }
36478
+ /** Files moved to the association directory during the current association. */
36479
+ get files() {
36480
+ return this._files;
36481
+ }
36482
+ /** Byte sizes parallel to files[], for transfer stats. */
36483
+ get fileSizes() {
36484
+ return this._fileSizes;
36485
+ }
36486
+ /** Captured output lines during the current association. */
36487
+ get outputLines() {
36488
+ return this._outputLines;
36489
+ }
36490
+ /** Marks the worker busy with a new association context. */
36491
+ beginAssociation(ctx) {
36492
+ this._state = "busy";
36493
+ this._context = ctx;
36494
+ this._files.length = 0;
36495
+ this._fileSizes.length = 0;
36496
+ this._outputLines.length = 0;
36497
+ this._pending.clear();
36498
+ }
36499
+ /** Returns the worker to idle and clears all association state. */
36500
+ endAssociation() {
36501
+ this._state = "idle";
36502
+ this._context = void 0;
36503
+ this._files.length = 0;
36504
+ this._fileSizes.length = 0;
36505
+ this._outputLines.length = 0;
36506
+ this._pending.clear();
36507
+ this._remoteSocket = void 0;
36508
+ this._workerSocket = void 0;
36509
+ }
36510
+ /** Tracks an in-flight file handling promise; auto-removes on completion. */
36511
+ trackFile(promise) {
36512
+ this._pending.add(promise);
36513
+ void promise.finally(() => {
36514
+ this._pending.delete(promise);
36515
+ });
36516
+ }
36517
+ /** Records a successfully received file path and its size. */
36518
+ recordFile(filePath, size) {
36519
+ this._files.push(filePath);
36520
+ this._fileSizes.push(size);
36521
+ }
36522
+ /** Awaits all in-flight file handling promises. */
36523
+ async drainPendingFiles() {
36524
+ if (this._pending.size > 0) {
36525
+ await Promise.all(this._pending);
36526
+ }
36527
+ }
36528
+ /** Appends a line to the output buffer, respecting the cap. */
36529
+ captureOutput(text) {
36530
+ if (this._outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
36531
+ this._outputLines.push(text);
36532
+ }
36533
+ }
36534
+ /** Stores the remote and worker sockets for later cleanup. */
36535
+ setSockets(remote, worker) {
36536
+ this._remoteSocket = remote;
36537
+ this._workerSocket = worker;
36538
+ }
36539
+ /** Destroys both sockets if they exist and are not already destroyed. */
36540
+ destroySockets() {
36541
+ if (this._remoteSocket !== void 0 && !this._remoteSocket.destroyed) {
36542
+ this._remoteSocket.destroy();
36543
+ }
36544
+ if (this._workerSocket !== void 0 && !this._workerSocket.destroyed) {
36545
+ this._workerSocket.destroy();
36546
+ }
36547
+ }
36548
+ };
36275
36549
  function allocatePort() {
36276
36550
  return new Promise((resolve) => {
36277
36551
  const server = net.createServer();
@@ -36289,6 +36563,13 @@ function allocatePort() {
36289
36563
  });
36290
36564
  });
36291
36565
  }
36566
+ function sumArray(arr) {
36567
+ let total = 0;
36568
+ for (let i = 0; i < arr.length; i++) {
36569
+ total += arr[i] ?? 0;
36570
+ }
36571
+ return total;
36572
+ }
36292
36573
  var DicomReceiver = class _DicomReceiver extends EventEmitter {
36293
36574
  constructor(options) {
36294
36575
  super();
@@ -36303,8 +36584,6 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36303
36584
  __publicField(this, "stopping", false);
36304
36585
  __publicField(this, "abortHandler");
36305
36586
  this.setMaxListeners(20);
36306
- this.on("error", () => {
36307
- });
36308
36587
  this.options = options;
36309
36588
  this.minPoolSize = options.minPoolSize ?? DEFAULT_MIN_POOL_SIZE;
36310
36589
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
@@ -36512,14 +36791,12 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36512
36791
  this.emit("error", { error: mkdirResult.error });
36513
36792
  return;
36514
36793
  }
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();
36794
+ const ctx = {
36795
+ associationId,
36796
+ associationDir,
36797
+ startAt: Date.now()
36798
+ };
36799
+ worker.beginAssociation(ctx);
36523
36800
  this.pipeConnection(worker, remoteSocket);
36524
36801
  void this.replenishPool();
36525
36802
  }
@@ -36529,7 +36806,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36529
36806
  if (idle !== void 0) return idle;
36530
36807
  const maxRetries = Math.min(Math.ceil(this.connectionTimeoutMs / CONNECTION_RETRY_INTERVAL_MS), MAX_CONNECTION_RETRIES);
36531
36808
  for (let i = 0; i < maxRetries; i++) {
36532
- await delay(CONNECTION_RETRY_INTERVAL_MS);
36809
+ await setTimeout$1(CONNECTION_RETRY_INTERVAL_MS);
36533
36810
  const found = this.getIdleWorker();
36534
36811
  if (found !== void 0) return found;
36535
36812
  if (this.stopping) return void 0;
@@ -36546,8 +36823,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36546
36823
  /** Pipes remote socket bidirectionally to the worker's port. */
36547
36824
  pipeConnection(worker, remoteSocket) {
36548
36825
  const workerSocket = net.createConnection({ port: worker.port, host: "127.0.0.1" });
36549
- worker.remoteSocket = remoteSocket;
36550
- worker.workerSocket = workerSocket;
36826
+ worker.setSockets(remoteSocket, workerSocket);
36551
36827
  const cleanup = () => {
36552
36828
  remoteSocket.unpipe(workerSocket);
36553
36829
  workerSocket.unpipe(remoteSocket);
@@ -36606,21 +36882,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36606
36882
  const createResult = this.createDcmrecv(port, tempDir);
36607
36883
  if (!createResult.ok) return createResult;
36608
36884
  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
- };
36885
+ const worker = new Worker(dcmrecv, port, tempDir);
36624
36886
  this.wireWorkerEvents(worker);
36625
36887
  const startResult = await dcmrecv.start();
36626
36888
  if (!startResult.ok) {
@@ -36629,14 +36891,9 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36629
36891
  this.workers.set(port, worker);
36630
36892
  return ok(worker);
36631
36893
  }
36632
- /** Stops a single worker: stop process, clean temp dir, remove from pool. */
36894
+ /** Stops a single worker: destroy sockets, stop process, clean temp dir, remove from pool. */
36633
36895
  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
- }
36896
+ worker.destroySockets();
36640
36897
  await worker.dcmrecv.stop();
36641
36898
  worker.dcmrecv[Symbol.dispose]();
36642
36899
  await removeDirSafe(worker.tempDir);
@@ -36688,12 +36945,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36688
36945
  this.wireRefusingAssociation(worker);
36689
36946
  this.wireOutputCapture(worker);
36690
36947
  }
36691
- /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
36948
+ /** Wires FILE_RECEIVED from dcmrecv worker captures context synchronously. */
36692
36949
  wireFileReceived(worker) {
36693
36950
  worker.dcmrecv.onFileReceived((data) => {
36694
- const assocId = worker.associationId;
36695
- const assocDir = worker.associationDir;
36696
- if (assocDir === void 0 || assocId === void 0) {
36951
+ const ctx = worker.context;
36952
+ if (ctx === void 0) {
36697
36953
  this.emit("error", {
36698
36954
  error: new Error(`DicomReceiver: FILE_RECEIVED with no active association (worker state: ${worker.state})`),
36699
36955
  filePath: data.filePath,
@@ -36703,22 +36959,21 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36703
36959
  });
36704
36960
  return;
36705
36961
  }
36706
- const promise = this.handleFileReceived(worker, data, assocId, assocDir);
36707
- worker.pendingFiles.push(promise);
36708
- void promise.finally(() => removePending(worker.pendingFiles, promise));
36962
+ const promise = this.handleFileReceived(worker, data, ctx);
36963
+ worker.trackFile(promise);
36709
36964
  });
36710
36965
  }
36711
36966
  /** Moves a received file, opens it as DicomInstance, and emits FILE_RECEIVED. */
36712
- async handleFileReceived(worker, data, assocId, assocDir) {
36967
+ async handleFileReceived(worker, data, ctx) {
36713
36968
  try {
36714
- await this.moveAndEmitFile(worker, data, assocId, assocDir);
36969
+ await this.moveAndEmitFile(worker, data, ctx);
36715
36970
  } catch (thrown) {
36716
36971
  const error = thrown instanceof Error ? thrown : new Error(String(thrown));
36717
36972
  this.emit("error", {
36718
36973
  error,
36719
36974
  filePath: data.filePath,
36720
- associationId: assocId,
36721
- associationDir: assocDir,
36975
+ associationId: ctx.associationId,
36976
+ associationDir: ctx.associationDir,
36722
36977
  callingAE: data.callingAE,
36723
36978
  calledAE: data.calledAE,
36724
36979
  source: data.source
@@ -36726,21 +36981,20 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36726
36981
  }
36727
36982
  }
36728
36983
  /** Inner handler: move file, parse DICOM, emit FILE_RECEIVED or error. */
36729
- async moveAndEmitFile(worker, data, assocId, assocDir) {
36984
+ async moveAndEmitFile(worker, data, ctx) {
36730
36985
  const srcPath = data.filePath;
36731
- const destPath = path.join(assocDir, path.basename(srcPath));
36986
+ const destPath = path.join(ctx.associationDir, path.basename(srcPath));
36732
36987
  const moveResult = await moveFile(srcPath, destPath);
36733
36988
  const finalPath = moveResult.ok ? destPath : srcPath;
36734
36989
  const fileSize = await statFileSafe(finalPath);
36735
- worker.files.push(finalPath);
36736
- worker.fileSizes.push(fileSize);
36990
+ worker.recordFile(finalPath, fileSize);
36737
36991
  const openResult = await DicomInstance.open(finalPath);
36738
36992
  if (!openResult.ok) {
36739
36993
  this.emit("error", {
36740
36994
  error: openResult.error,
36741
36995
  filePath: finalPath,
36742
- associationId: assocId,
36743
- associationDir: assocDir,
36996
+ associationId: ctx.associationId,
36997
+ associationDir: ctx.associationDir,
36744
36998
  callingAE: data.callingAE,
36745
36999
  calledAE: data.calledAE,
36746
37000
  source: data.source
@@ -36750,8 +37004,8 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36750
37004
  this.emit("FILE_RECEIVED", {
36751
37005
  filePath: finalPath,
36752
37006
  fileSize,
36753
- associationId: assocId,
36754
- associationDir: assocDir,
37007
+ associationId: ctx.associationId,
37008
+ associationDir: ctx.associationDir,
36755
37009
  callingAE: data.callingAE,
36756
37010
  calledAE: data.calledAE,
36757
37011
  source: data.source,
@@ -36766,21 +37020,20 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36766
37020
  }
36767
37021
  /** Awaits ALL pending file operations, emits ASSOCIATION_COMPLETE, resets worker state. */
36768
37022
  async finalizeAssociation(worker, data) {
36769
- while (worker.pendingFiles.length > 0) {
36770
- await Promise.all(worker.pendingFiles);
36771
- }
37023
+ await worker.drainPendingFiles();
36772
37024
  this.emitAssociationComplete(worker, data);
36773
- this.resetWorker(worker);
37025
+ worker.endAssociation();
36774
37026
  void this.scaleDown();
36775
37027
  }
36776
37028
  /** Emits the ASSOCIATION_COMPLETE event with transfer stats. */
36777
37029
  emitAssociationComplete(worker, data) {
36778
- const assocId = worker.associationId ?? data.associationId;
36779
- const assocDir = worker.associationDir ?? "";
37030
+ const ctx = worker.context;
37031
+ const assocId = ctx?.associationId ?? data.associationId;
37032
+ const assocDir = ctx?.associationDir ?? "";
37033
+ const startAt = ctx?.startAt ?? Date.now();
36780
37034
  const files = [...worker.files];
36781
37035
  const output = [...worker.outputLines];
36782
37036
  const endAt = Date.now();
36783
- const startAt = worker.startAt ?? endAt;
36784
37037
  const totalBytes = sumArray(worker.fileSizes);
36785
37038
  const elapsedMs = endAt - startAt;
36786
37039
  const bytesPerSecond = elapsedMs > 0 ? Math.round(totalBytes / elapsedMs * 1e3) : 0;
@@ -36800,24 +37053,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36800
37053
  output
36801
37054
  });
36802
37055
  }
36803
- /** Resets worker state to idle after an association completes. */
36804
- resetWorker(worker) {
36805
- worker.state = "idle";
36806
- worker.associationId = void 0;
36807
- worker.associationDir = void 0;
36808
- worker.files = [];
36809
- worker.fileSizes = [];
36810
- worker.outputLines = [];
36811
- worker.pendingFiles = [];
36812
- worker.startAt = void 0;
36813
- worker.remoteSocket = void 0;
36814
- worker.workerSocket = void 0;
36815
- }
36816
37056
  /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
36817
37057
  wireAssociationReceived(worker) {
36818
37058
  worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
36819
37059
  this.emit("ASSOCIATION_RECEIVED", {
36820
- associationId: worker.associationId ?? "",
37060
+ associationId: worker.context?.associationId ?? "",
36821
37061
  callingAE: data.callingAE,
36822
37062
  calledAE: data.calledAE,
36823
37063
  source: data.source
@@ -36828,7 +37068,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36828
37068
  wireCStoreRequest(worker) {
36829
37069
  worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
36830
37070
  this.emit("C_STORE_REQUEST", {
36831
- associationId: worker.associationId ?? "",
37071
+ associationId: worker.context?.associationId ?? "",
36832
37072
  raw: data.raw
36833
37073
  });
36834
37074
  });
@@ -36837,7 +37077,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36837
37077
  wireEchoRequest(worker) {
36838
37078
  worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
36839
37079
  this.emit("ECHO_REQUEST", {
36840
- associationId: worker.associationId ?? ""
37080
+ associationId: worker.context?.associationId ?? ""
36841
37081
  });
36842
37082
  });
36843
37083
  }
@@ -36852,8 +37092,8 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36852
37092
  /** Captures worker output lines during busy associations. */
36853
37093
  wireOutputCapture(worker) {
36854
37094
  worker.dcmrecv.on("line", ({ text }) => {
36855
- if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
36856
- worker.outputLines.push(text);
37095
+ if (worker.state === "busy") {
37096
+ worker.captureOutput(text);
36857
37097
  }
36858
37098
  });
36859
37099
  }
@@ -36872,60 +37112,6 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
36872
37112
  signal.addEventListener("abort", this.abortHandler, { once: true });
36873
37113
  }
36874
37114
  };
36875
- async function ensureDirectory(dirPath) {
36876
- try {
36877
- await fs.mkdir(dirPath, { recursive: true });
36878
- return ok(void 0);
36879
- } catch (e) {
36880
- const msg = e instanceof Error ? e.message : String(e);
36881
- return err(new Error(`Failed to create directory ${dirPath}: ${msg}`));
36882
- }
36883
- }
36884
- async function moveFile(src, dest) {
36885
- try {
36886
- await fs.rename(src, dest);
36887
- return ok(void 0);
36888
- } catch {
36889
- try {
36890
- await fs.copyFile(src, dest);
36891
- await fs.unlink(src);
36892
- return ok(void 0);
36893
- } catch (e) {
36894
- const msg = e instanceof Error ? e.message : String(e);
36895
- return err(new Error(`Failed to move file ${src} \u2192 ${dest}: ${msg}`));
36896
- }
36897
- }
36898
- }
36899
- async function statFileSafe(filePath) {
36900
- try {
36901
- const stat4 = await fs.stat(filePath);
36902
- return stat4.size;
36903
- } catch {
36904
- return 0;
36905
- }
36906
- }
36907
- function sumArray(arr) {
36908
- let total = 0;
36909
- for (let i = 0; i < arr.length; i++) {
36910
- total += arr[i] ?? 0;
36911
- }
36912
- return total;
36913
- }
36914
- async function removeDirSafe(dirPath) {
36915
- try {
36916
- await fs.rm(dirPath, { recursive: true, force: true });
36917
- } catch {
36918
- }
36919
- }
36920
- function removePending(arr, promise) {
36921
- const idx = arr.indexOf(promise);
36922
- if (idx !== -1) void arr.splice(idx, 1);
36923
- }
36924
- function delay(ms) {
36925
- return new Promise((resolve) => {
36926
- setTimeout(resolve, ms);
36927
- });
36928
- }
36929
37115
 
36930
37116
  // src/senders/types.ts
36931
37117
  var SenderMode = {
@@ -37348,7 +37534,7 @@ function createStorescuExecutor(options) {
37348
37534
  return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37349
37535
  };
37350
37536
  }
37351
- function resolveConfig(options) {
37537
+ function resolveConfig2(options) {
37352
37538
  const mode = options.mode ?? "multiple";
37353
37539
  const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
37354
37540
  return {
@@ -37395,7 +37581,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37395
37581
  if (!validation.success) {
37396
37582
  return err(createValidationError("DicomSender", validation.error));
37397
37583
  }
37398
- const cfg = resolveConfig(options);
37584
+ const cfg = resolveConfig2(options);
37399
37585
  const senderRef = { current: void 0 };
37400
37586
  const engine = new SenderEngine({
37401
37587
  ...cfg,
@@ -37597,7 +37783,7 @@ function createDcmsendExecutor(options) {
37597
37783
  return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37598
37784
  };
37599
37785
  }
37600
- function resolveConfig2(options) {
37786
+ function resolveConfig3(options) {
37601
37787
  const mode = options.mode ?? "multiple";
37602
37788
  const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS2;
37603
37789
  return {
@@ -37644,7 +37830,7 @@ var DicomSend = class _DicomSend extends EventEmitter {
37644
37830
  if (!validation.success) {
37645
37831
  return err(createValidationError("DicomSend", validation.error));
37646
37832
  }
37647
- const cfg = resolveConfig2(options);
37833
+ const cfg = resolveConfig3(options);
37648
37834
  const senderRef = { current: void 0 };
37649
37835
  const engine = new SenderEngine({
37650
37836
  ...cfg,
@@ -38245,70 +38431,6 @@ var PacsClient = class _PacsClient {
38245
38431
  return ok({ success: true, outputDirectory: options.outputDirectory });
38246
38432
  }
38247
38433
  };
38248
- var MIN_CONCURRENCY = 1;
38249
- var MAX_CONCURRENCY = 64;
38250
- var DEFAULT_CONCURRENCY = 4;
38251
- function clampConcurrency(value) {
38252
- if (value === void 0) return DEFAULT_CONCURRENCY;
38253
- return Math.max(MIN_CONCURRENCY, Math.min(MAX_CONCURRENCY, Math.floor(value)));
38254
- }
38255
- function createBatchState(total) {
38256
- const results = [];
38257
- for (let i = 0; i < total; i += 1) {
38258
- results.push(void 0);
38259
- }
38260
- return { results, succeeded: 0, failed: 0, completed: 0 };
38261
- }
38262
- async function processItem(item, index, operation, state) {
38263
- let result;
38264
- try {
38265
- result = await operation(item);
38266
- } catch (thrown) {
38267
- result = err(stderr(thrown));
38268
- }
38269
- state.results[index] = result;
38270
- state.completed += 1;
38271
- if (result.ok) {
38272
- state.succeeded += 1;
38273
- } else {
38274
- state.failed += 1;
38275
- }
38276
- }
38277
- async function batch(items, operation, options) {
38278
- const concurrency = clampConcurrency(options?.concurrency);
38279
- const signal = options?.signal;
38280
- const onProgress = options?.onProgress;
38281
- const total = items.length;
38282
- if (total === 0) {
38283
- return { results: [], succeeded: 0, failed: 0 };
38284
- }
38285
- const state = createBatchState(total);
38286
- const inFlight = /* @__PURE__ */ new Set();
38287
- let nextIndex = 0;
38288
- while (nextIndex < total) {
38289
- if (signal?.aborted) break;
38290
- const itemIndex = nextIndex;
38291
- nextIndex += 1;
38292
- const promise = processItem(items[itemIndex], itemIndex, operation, state).then(() => {
38293
- if (onProgress) {
38294
- const result = state.results[itemIndex];
38295
- onProgress(state.completed, total, result);
38296
- }
38297
- });
38298
- inFlight.add(promise);
38299
- promise.then(
38300
- () => inFlight.delete(promise),
38301
- () => inFlight.delete(promise)
38302
- );
38303
- if (inFlight.size >= concurrency) {
38304
- await Promise.race(inFlight);
38305
- }
38306
- }
38307
- if (inFlight.size > 0) {
38308
- await Promise.all(inFlight);
38309
- }
38310
- return { results: state.results, succeeded: state.succeeded, failed: state.failed };
38311
- }
38312
38434
 
38313
38435
  // src/testing/types.ts
38314
38436
  var HammerPhase = {
@@ -38645,80 +38767,6 @@ var DicomHammer = class _DicomHammer extends EventEmitter {
38645
38767
  }
38646
38768
  };
38647
38769
 
38648
- // src/utils/retry.ts
38649
- var DEFAULT_MAX_ATTEMPTS = 3;
38650
- var DEFAULT_INITIAL_DELAY_MS = 1e3;
38651
- var DEFAULT_MAX_DELAY_MS = 3e4;
38652
- var DEFAULT_BACKOFF_MULTIPLIER = 2;
38653
- var JITTER_FRACTION = 0.1;
38654
- var DEFAULT_CONFIG = {
38655
- maxAttempts: DEFAULT_MAX_ATTEMPTS,
38656
- initialDelayMs: DEFAULT_INITIAL_DELAY_MS,
38657
- maxDelayMs: DEFAULT_MAX_DELAY_MS,
38658
- backoffMultiplier: DEFAULT_BACKOFF_MULTIPLIER,
38659
- shouldRetry: void 0,
38660
- signal: void 0,
38661
- onRetry: void 0
38662
- };
38663
- function resolveConfig3(opts) {
38664
- if (!opts) return DEFAULT_CONFIG;
38665
- return {
38666
- ...DEFAULT_CONFIG,
38667
- ...Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0)),
38668
- maxAttempts: Math.max(1, Math.floor(opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS)),
38669
- initialDelayMs: Math.max(0, opts.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS),
38670
- maxDelayMs: Math.max(0, opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS),
38671
- backoffMultiplier: Math.max(1, opts.backoffMultiplier ?? DEFAULT_BACKOFF_MULTIPLIER)
38672
- };
38673
- }
38674
- function computeDelay(attempt, config) {
38675
- const baseDelay = Math.min(config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt), config.maxDelayMs);
38676
- const jitter = baseDelay * JITTER_FRACTION * (2 * Math.random() - 1);
38677
- return Math.max(0, Math.round(baseDelay + jitter));
38678
- }
38679
- async function waitWithAbort(delayMs, signal) {
38680
- if (signal?.aborted) {
38681
- return false;
38682
- }
38683
- return new Promise((resolve) => {
38684
- const timer = setTimeout(() => {
38685
- cleanup();
38686
- resolve(true);
38687
- }, delayMs);
38688
- const onAbort = () => {
38689
- clearTimeout(timer);
38690
- cleanup();
38691
- resolve(false);
38692
- };
38693
- const cleanup = () => {
38694
- signal?.removeEventListener("abort", onAbort);
38695
- };
38696
- if (signal) {
38697
- signal.addEventListener("abort", onAbort, { once: true });
38698
- }
38699
- });
38700
- }
38701
- function shouldBreakAfterFailure(attempt, lastError, config) {
38702
- if (attempt === config.maxAttempts - 1) return true;
38703
- if (config.shouldRetry && !config.shouldRetry(lastError, attempt + 1)) return true;
38704
- if (config.signal?.aborted) return true;
38705
- return false;
38706
- }
38707
- async function retry(operation, options) {
38708
- const config = resolveConfig3(options);
38709
- let lastResult = err(new Error("No attempts executed"));
38710
- for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
38711
- lastResult = await operation();
38712
- if (lastResult.ok) return lastResult;
38713
- if (shouldBreakAfterFailure(attempt, lastResult.error, config)) break;
38714
- const delayMs = computeDelay(attempt, config);
38715
- if (config.onRetry) config.onRetry(lastResult.error, attempt + 1, delayMs);
38716
- const waitCompleted = await waitWithAbort(delayMs, config.signal);
38717
- if (!waitCompleted) break;
38718
- }
38719
- return lastResult;
38720
- }
38721
-
38722
38770
  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 };
38723
38771
  //# sourceMappingURL=index.js.map
38724
38772
  //# sourceMappingURL=index.js.map