@ubercode/dcmtk 0.4.0 → 0.5.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
@@ -36119,8 +36119,9 @@ var DEFAULT_MAX_POOL_SIZE = 10;
36119
36119
  var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
36120
36120
  var CONNECTION_RETRY_INTERVAL_MS = 500;
36121
36121
  var MAX_CONNECTION_RETRIES = 200;
36122
+ var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
36122
36123
  var DicomReceiverOptionsSchema = zod.z.object({
36123
- port: zod.z.number().int().min(1).max(65535),
36124
+ port: zod.z.number().int().min(0).max(65535),
36124
36125
  storageDir: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
36125
36126
  aeTitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
36126
36127
  minPoolSize: zod.z.number().int().min(1).max(100).optional(),
@@ -36128,6 +36129,9 @@ var DicomReceiverOptionsSchema = zod.z.object({
36128
36129
  connectionTimeoutMs: zod.z.number().int().positive().optional(),
36129
36130
  configFile: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
36130
36131
  configProfile: zod.z.string().min(1).optional(),
36132
+ acseTimeout: zod.z.number().int().positive().optional(),
36133
+ dimseTimeout: zod.z.number().int().positive().optional(),
36134
+ maxPdu: zod.z.number().int().min(4096).max(131072).optional(),
36131
36135
  signal: zod.z.instanceof(AbortSignal).optional()
36132
36136
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
36133
36137
  message: "minPoolSize must be <= maxPoolSize"
@@ -36256,6 +36260,57 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36256
36260
  onAssociationComplete(listener) {
36257
36261
  return this.on("ASSOCIATION_COMPLETE", listener);
36258
36262
  }
36263
+ /**
36264
+ * Registers a listener for new associations (bubbled from workers).
36265
+ *
36266
+ * @param listener - Callback receiving association received data
36267
+ * @returns this for chaining
36268
+ */
36269
+ onAssociationReceived(listener) {
36270
+ return this.on("ASSOCIATION_RECEIVED", listener);
36271
+ }
36272
+ /**
36273
+ * Registers a listener for C-STORE requests (bubbled from workers).
36274
+ *
36275
+ * @param listener - Callback receiving C-STORE request data
36276
+ * @returns this for chaining
36277
+ */
36278
+ onCStoreRequest(listener) {
36279
+ return this.on("C_STORE_REQUEST", listener);
36280
+ }
36281
+ /**
36282
+ * Registers a listener for C-ECHO requests (bubbled from workers).
36283
+ *
36284
+ * @param listener - Callback receiving echo request data
36285
+ * @returns this for chaining
36286
+ */
36287
+ onEchoRequest(listener) {
36288
+ return this.on("ECHO_REQUEST", listener);
36289
+ }
36290
+ /**
36291
+ * Registers a listener for refused associations (bubbled from workers).
36292
+ *
36293
+ * @param listener - Callback receiving refusing association data
36294
+ * @returns this for chaining
36295
+ */
36296
+ onRefusingAssociation(listener) {
36297
+ return this.on("REFUSING_ASSOCIATION", listener);
36298
+ }
36299
+ /**
36300
+ * Routes an external socket to an idle worker.
36301
+ *
36302
+ * The pool must be started via `start()` before calling this method.
36303
+ * Use this when managing your own TCP listener (e.g., protocol router).
36304
+ *
36305
+ * @param socket - An incoming net.Socket to route to a worker
36306
+ */
36307
+ handleSocket(socket) {
36308
+ if (!this.started) {
36309
+ socket.destroy(new Error("DicomReceiver: not started"));
36310
+ return;
36311
+ }
36312
+ void this.handleConnection(socket);
36313
+ }
36259
36314
  /** Current pool status. */
36260
36315
  get poolStatus() {
36261
36316
  let idle = 0;
@@ -36269,8 +36324,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36269
36324
  // -----------------------------------------------------------------------
36270
36325
  // TCP proxy
36271
36326
  // -----------------------------------------------------------------------
36272
- /** Starts the TCP proxy on the configured port. */
36327
+ /** Starts the TCP proxy on the configured port. Skips if port is 0. */
36273
36328
  startTcpProxy() {
36329
+ if (this.options.port === 0) {
36330
+ return Promise.resolve(ok(void 0));
36331
+ }
36274
36332
  return new Promise((resolve) => {
36275
36333
  this.tcpServer = net__namespace.createServer((socket) => {
36276
36334
  void this.handleConnection(socket);
@@ -36323,6 +36381,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36323
36381
  worker.associationDir = associationDir;
36324
36382
  worker.files = [];
36325
36383
  worker.fileSizes = [];
36384
+ worker.outputLines = [];
36326
36385
  worker.startAt = Date.now();
36327
36386
  this.pipeConnection(worker, remoteSocket);
36328
36387
  void this.replenishPool();
@@ -36381,6 +36440,19 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36381
36440
  }
36382
36441
  return ok(void 0);
36383
36442
  }
36443
+ /** Creates a Dcmrecv instance with the pool's shared options. */
36444
+ createDcmrecv(port, tempDir) {
36445
+ return Dcmrecv.create({
36446
+ port,
36447
+ aeTitle: this.options.aeTitle ?? "DCMRECV",
36448
+ outputDirectory: tempDir,
36449
+ configFile: this.options.configFile,
36450
+ configProfile: this.options.configProfile,
36451
+ acseTimeout: this.options.acseTimeout,
36452
+ dimseTimeout: this.options.dimseTimeout,
36453
+ maxPdu: this.options.maxPdu
36454
+ });
36455
+ }
36384
36456
  /** Spawns a single Dcmrecv worker with an ephemeral port. */
36385
36457
  async spawnWorker() {
36386
36458
  const portResult = await allocatePort();
@@ -36389,13 +36461,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36389
36461
  const tempDir = path__namespace.join(os__namespace.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
36390
36462
  const mkdirResult = await ensureDirectory(tempDir);
36391
36463
  if (!mkdirResult.ok) return mkdirResult;
36392
- const createResult = Dcmrecv.create({
36393
- port,
36394
- aeTitle: this.options.aeTitle ?? "DCMRECV",
36395
- outputDirectory: tempDir,
36396
- configFile: this.options.configFile,
36397
- configProfile: this.options.configProfile
36398
- });
36464
+ const createResult = this.createDcmrecv(port, tempDir);
36399
36465
  if (!createResult.ok) return createResult;
36400
36466
  const dcmrecv = createResult.value;
36401
36467
  const worker = {
@@ -36407,6 +36473,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36407
36473
  associationDir: void 0,
36408
36474
  files: [],
36409
36475
  fileSizes: [],
36476
+ outputLines: [],
36410
36477
  startAt: void 0,
36411
36478
  remoteSocket: void 0,
36412
36479
  workerSocket: void 0
@@ -36468,10 +36535,15 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36468
36535
  // -----------------------------------------------------------------------
36469
36536
  // Worker event wiring
36470
36537
  // -----------------------------------------------------------------------
36471
- /** Wires FILE_RECEIVED and ASSOCIATION_COMPLETE events on a worker. */
36538
+ /** Wires all events on a worker: file handling, association lifecycle, output capture. */
36472
36539
  wireWorkerEvents(worker) {
36473
36540
  this.wireFileReceived(worker);
36474
36541
  this.wireAssociationComplete(worker);
36542
+ this.wireAssociationReceived(worker);
36543
+ this.wireCStoreRequest(worker);
36544
+ this.wireEchoRequest(worker);
36545
+ this.wireRefusingAssociation(worker);
36546
+ this.wireOutputCapture(worker);
36475
36547
  }
36476
36548
  /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
36477
36549
  wireFileReceived(worker) {
@@ -36519,6 +36591,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36519
36591
  const assocId = worker.associationId ?? data.associationId;
36520
36592
  const assocDir = worker.associationDir ?? "";
36521
36593
  const files = [...worker.files];
36594
+ const output = [...worker.outputLines];
36522
36595
  const endAt = Date.now();
36523
36596
  const startAt = worker.startAt ?? endAt;
36524
36597
  const totalBytes = sumArray(worker.fileSizes);
@@ -36536,19 +36609,65 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
36536
36609
  totalBytes,
36537
36610
  bytesPerSecond,
36538
36611
  startAt,
36539
- endAt
36612
+ endAt,
36613
+ output
36540
36614
  });
36541
36615
  worker.state = "idle";
36542
36616
  worker.associationId = void 0;
36543
36617
  worker.associationDir = void 0;
36544
36618
  worker.files = [];
36545
36619
  worker.fileSizes = [];
36620
+ worker.outputLines = [];
36546
36621
  worker.startAt = void 0;
36547
36622
  worker.remoteSocket = void 0;
36548
36623
  worker.workerSocket = void 0;
36549
36624
  void this.scaleDown();
36550
36625
  });
36551
36626
  }
36627
+ /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
36628
+ wireAssociationReceived(worker) {
36629
+ worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
36630
+ this.emit("ASSOCIATION_RECEIVED", {
36631
+ associationId: worker.associationId ?? "",
36632
+ callingAE: data.callingAE,
36633
+ calledAE: data.calledAE,
36634
+ source: data.source
36635
+ });
36636
+ });
36637
+ }
36638
+ /** Bubbles C_STORE_REQUEST from dcmrecv worker. */
36639
+ wireCStoreRequest(worker) {
36640
+ worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
36641
+ this.emit("C_STORE_REQUEST", {
36642
+ associationId: worker.associationId ?? "",
36643
+ raw: data.raw
36644
+ });
36645
+ });
36646
+ }
36647
+ /** Bubbles ECHO_REQUEST from dcmrecv worker. */
36648
+ wireEchoRequest(worker) {
36649
+ worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
36650
+ this.emit("ECHO_REQUEST", {
36651
+ associationId: worker.associationId ?? ""
36652
+ });
36653
+ });
36654
+ }
36655
+ /** Bubbles REFUSING_ASSOCIATION from dcmrecv worker. */
36656
+ wireRefusingAssociation(worker) {
36657
+ worker.dcmrecv.onEvent("REFUSING_ASSOCIATION", (data) => {
36658
+ this.emit("REFUSING_ASSOCIATION", {
36659
+ reason: data.reason
36660
+ });
36661
+ });
36662
+ }
36663
+ /** Captures worker output lines during busy associations. */
36664
+ wireOutputCapture(worker) {
36665
+ worker.dcmrecv.on("line", ({ text }) => {
36666
+ if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
36667
+ worker.outputLines.push(text);
36668
+ }
36669
+ });
36670
+ }
36552
36671
  // -----------------------------------------------------------------------
36553
36672
  // Abort signal
36554
36673
  // -----------------------------------------------------------------------