@ubercode/dcmtk 0.13.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/servers.cjs CHANGED
@@ -527,7 +527,19 @@ function truncate(value, maxLength) {
527
527
  }
528
528
  return `${value.substring(0, maxLength)}...`;
529
529
  }
530
- function createToolError(toolName, args, exitCode, stderr6) {
530
+ var ToolExecutionError = class extends Error {
531
+ constructor(message, details) {
532
+ super(message);
533
+ __publicField(this, "stdout");
534
+ __publicField(this, "stderr");
535
+ __publicField(this, "exitCode");
536
+ this.name = "ToolExecutionError";
537
+ this.stdout = details.stdout;
538
+ this.stderr = details.stderr;
539
+ this.exitCode = details.exitCode;
540
+ }
541
+ };
542
+ function createToolError(toolName, args, exitCode, stderr6, stdout = "") {
531
543
  const argsStr = truncate(args.join(" "), MAX_ARGS_LENGTH);
532
544
  const stderrStr = truncate(stderr6.trim(), MAX_STDERR_LENGTH);
533
545
  const parts = [`${toolName} failed (exit code ${String(exitCode)})`];
@@ -537,7 +549,7 @@ function createToolError(toolName, args, exitCode, stderr6) {
537
549
  if (stderrStr.length > 0) {
538
550
  parts.push(`stderr: ${stderrStr}`);
539
551
  }
540
- return new Error(parts.join(" | "));
552
+ return new ToolExecutionError(parts.join(" | "), { stdout, stderr: stderr6, exitCode });
541
553
  }
542
554
  function createValidationError(toolName, zodError) {
543
555
  const parts = [];
@@ -2396,15 +2408,17 @@ async function dcm2json(inputPath, options) {
2396
2408
  }
2397
2409
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2398
2410
  const signal = options?.signal;
2411
+ const startTime = Date.now();
2412
+ const remaining = () => Math.max(1e3, timeoutMs - (Date.now() - startTime));
2399
2413
  const verbosity = options?.verbosity;
2400
2414
  if (options?.directOnly === true) {
2401
- return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
2415
+ return tryDirectPath(inputPath, remaining(), signal, verbosity);
2402
2416
  }
2403
- const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, buildXmlOpts(options));
2417
+ const xmlResult = await tryXmlPath(inputPath, remaining(), signal, buildXmlOpts(options));
2404
2418
  if (xmlResult.ok) {
2405
2419
  return xmlResult;
2406
2420
  }
2407
- return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
2421
+ return tryDirectPath(inputPath, remaining(), signal, verbosity);
2408
2422
  }
2409
2423
  var TAG_OR_PATH_PATTERN = /^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\]\.\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\))*(\[\d+\])?$/;
2410
2424
  var TagModificationSchema = zod.z.object({
@@ -2888,7 +2902,7 @@ var Worker = class {
2888
2902
  this.port = port;
2889
2903
  this.tempDir = tempDir;
2890
2904
  }
2891
- /** Current worker state. */
2905
+ /** Current worker state: idle (ready), busy (handling association), finalizing (draining). */
2892
2906
  get state() {
2893
2907
  return this._state;
2894
2908
  }
@@ -2920,6 +2934,10 @@ var Worker = class {
2920
2934
  this._instancesReceived = 0;
2921
2935
  this._instanceErrors = 0;
2922
2936
  }
2937
+ /** Marks the worker as finalizing — still has valid context but cannot accept new connections. */
2938
+ markFinalizing() {
2939
+ this._state = "finalizing";
2940
+ }
2923
2941
  /**
2924
2942
  * Returns the worker to idle. Context is intentionally preserved so that
2925
2943
  * late FILE_RECEIVED events (arriving in a subsequent pipe chunk after
@@ -3102,6 +3120,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3102
3120
  this.options.signal.removeEventListener("abort", this.abortHandler);
3103
3121
  }
3104
3122
  await this.closeTcpProxy();
3123
+ await this.drainAllWorkers();
3105
3124
  const stopPromises = [];
3106
3125
  for (const worker of this.workers.values()) {
3107
3126
  stopPromises.push(this.stopWorker(worker));
@@ -3270,6 +3289,18 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3270
3289
  this.tcpServer.close(() => resolve());
3271
3290
  });
3272
3291
  }
3292
+ /** Drains in-flight file and instance work on all non-idle workers, with a 5s safety timeout. */
3293
+ async drainAllWorkers() {
3294
+ const drainPromises = [];
3295
+ for (const worker of this.workers.values()) {
3296
+ if (worker.state !== "idle") {
3297
+ drainPromises.push(worker.drainPendingFiles());
3298
+ drainPromises.push(worker.drainInstancePending());
3299
+ }
3300
+ }
3301
+ if (drainPromises.length === 0) return;
3302
+ await Promise.race([Promise.all(drainPromises), promises.setTimeout(5e3)]);
3303
+ }
3273
3304
  // -----------------------------------------------------------------------
3274
3305
  // Connection routing
3275
3306
  // -----------------------------------------------------------------------
@@ -3298,7 +3329,9 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3298
3329
  };
3299
3330
  worker.beginAssociation(ctx);
3300
3331
  this.pipeConnection(worker, remoteSocket);
3301
- void this.replenishPool();
3332
+ void this.replenishPool().catch((e) => {
3333
+ this.emit("error", { error: e instanceof Error ? e : new Error(String(e)) });
3334
+ });
3302
3335
  }
3303
3336
  /** Finds an idle worker, retrying up to connectionTimeoutMs. */
3304
3337
  async findIdleWorker() {
@@ -3537,6 +3570,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3537
3570
  /** Returns worker to idle pool on association complete, emits summary. */
3538
3571
  wireAssociationComplete(worker) {
3539
3572
  worker.dcmrecv.onAssociationComplete((data) => {
3573
+ worker.markFinalizing();
3540
3574
  setImmediate(() => {
3541
3575
  void this.finalizeAssociation(worker, data);
3542
3576
  });