@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/servers.js CHANGED
@@ -2675,8 +2675,9 @@ var DEFAULT_MAX_POOL_SIZE = 10;
2675
2675
  var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
2676
2676
  var CONNECTION_RETRY_INTERVAL_MS = 500;
2677
2677
  var MAX_CONNECTION_RETRIES = 200;
2678
+ var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
2678
2679
  var DicomReceiverOptionsSchema = z.object({
2679
- port: z.number().int().min(1).max(65535),
2680
+ port: z.number().int().min(0).max(65535),
2680
2681
  storageDir: z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
2681
2682
  aeTitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
2682
2683
  minPoolSize: z.number().int().min(1).max(100).optional(),
@@ -2684,6 +2685,9 @@ var DicomReceiverOptionsSchema = z.object({
2684
2685
  connectionTimeoutMs: z.number().int().positive().optional(),
2685
2686
  configFile: z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
2686
2687
  configProfile: z.string().min(1).optional(),
2688
+ acseTimeout: z.number().int().positive().optional(),
2689
+ dimseTimeout: z.number().int().positive().optional(),
2690
+ maxPdu: z.number().int().min(4096).max(131072).optional(),
2687
2691
  signal: z.instanceof(AbortSignal).optional()
2688
2692
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
2689
2693
  message: "minPoolSize must be <= maxPoolSize"
@@ -2812,6 +2816,57 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
2812
2816
  onAssociationComplete(listener) {
2813
2817
  return this.on("ASSOCIATION_COMPLETE", listener);
2814
2818
  }
2819
+ /**
2820
+ * Registers a listener for new associations (bubbled from workers).
2821
+ *
2822
+ * @param listener - Callback receiving association received data
2823
+ * @returns this for chaining
2824
+ */
2825
+ onAssociationReceived(listener) {
2826
+ return this.on("ASSOCIATION_RECEIVED", listener);
2827
+ }
2828
+ /**
2829
+ * Registers a listener for C-STORE requests (bubbled from workers).
2830
+ *
2831
+ * @param listener - Callback receiving C-STORE request data
2832
+ * @returns this for chaining
2833
+ */
2834
+ onCStoreRequest(listener) {
2835
+ return this.on("C_STORE_REQUEST", listener);
2836
+ }
2837
+ /**
2838
+ * Registers a listener for C-ECHO requests (bubbled from workers).
2839
+ *
2840
+ * @param listener - Callback receiving echo request data
2841
+ * @returns this for chaining
2842
+ */
2843
+ onEchoRequest(listener) {
2844
+ return this.on("ECHO_REQUEST", listener);
2845
+ }
2846
+ /**
2847
+ * Registers a listener for refused associations (bubbled from workers).
2848
+ *
2849
+ * @param listener - Callback receiving refusing association data
2850
+ * @returns this for chaining
2851
+ */
2852
+ onRefusingAssociation(listener) {
2853
+ return this.on("REFUSING_ASSOCIATION", listener);
2854
+ }
2855
+ /**
2856
+ * Routes an external socket to an idle worker.
2857
+ *
2858
+ * The pool must be started via `start()` before calling this method.
2859
+ * Use this when managing your own TCP listener (e.g., protocol router).
2860
+ *
2861
+ * @param socket - An incoming net.Socket to route to a worker
2862
+ */
2863
+ handleSocket(socket) {
2864
+ if (!this.started) {
2865
+ socket.destroy(new Error("DicomReceiver: not started"));
2866
+ return;
2867
+ }
2868
+ void this.handleConnection(socket);
2869
+ }
2815
2870
  /** Current pool status. */
2816
2871
  get poolStatus() {
2817
2872
  let idle = 0;
@@ -2825,8 +2880,11 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
2825
2880
  // -----------------------------------------------------------------------
2826
2881
  // TCP proxy
2827
2882
  // -----------------------------------------------------------------------
2828
- /** Starts the TCP proxy on the configured port. */
2883
+ /** Starts the TCP proxy on the configured port. Skips if port is 0. */
2829
2884
  startTcpProxy() {
2885
+ if (this.options.port === 0) {
2886
+ return Promise.resolve(ok(void 0));
2887
+ }
2830
2888
  return new Promise((resolve) => {
2831
2889
  this.tcpServer = net.createServer((socket) => {
2832
2890
  void this.handleConnection(socket);
@@ -2879,6 +2937,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
2879
2937
  worker.associationDir = associationDir;
2880
2938
  worker.files = [];
2881
2939
  worker.fileSizes = [];
2940
+ worker.outputLines = [];
2882
2941
  worker.startAt = Date.now();
2883
2942
  this.pipeConnection(worker, remoteSocket);
2884
2943
  void this.replenishPool();
@@ -2937,6 +2996,19 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
2937
2996
  }
2938
2997
  return ok(void 0);
2939
2998
  }
2999
+ /** Creates a Dcmrecv instance with the pool's shared options. */
3000
+ createDcmrecv(port, tempDir) {
3001
+ return Dcmrecv.create({
3002
+ port,
3003
+ aeTitle: this.options.aeTitle ?? "DCMRECV",
3004
+ outputDirectory: tempDir,
3005
+ configFile: this.options.configFile,
3006
+ configProfile: this.options.configProfile,
3007
+ acseTimeout: this.options.acseTimeout,
3008
+ dimseTimeout: this.options.dimseTimeout,
3009
+ maxPdu: this.options.maxPdu
3010
+ });
3011
+ }
2940
3012
  /** Spawns a single Dcmrecv worker with an ephemeral port. */
2941
3013
  async spawnWorker() {
2942
3014
  const portResult = await allocatePort();
@@ -2945,13 +3017,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
2945
3017
  const tempDir = path.join(os.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
2946
3018
  const mkdirResult = await ensureDirectory(tempDir);
2947
3019
  if (!mkdirResult.ok) return mkdirResult;
2948
- const createResult = Dcmrecv.create({
2949
- port,
2950
- aeTitle: this.options.aeTitle ?? "DCMRECV",
2951
- outputDirectory: tempDir,
2952
- configFile: this.options.configFile,
2953
- configProfile: this.options.configProfile
2954
- });
3020
+ const createResult = this.createDcmrecv(port, tempDir);
2955
3021
  if (!createResult.ok) return createResult;
2956
3022
  const dcmrecv = createResult.value;
2957
3023
  const worker = {
@@ -2963,6 +3029,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
2963
3029
  associationDir: void 0,
2964
3030
  files: [],
2965
3031
  fileSizes: [],
3032
+ outputLines: [],
2966
3033
  startAt: void 0,
2967
3034
  remoteSocket: void 0,
2968
3035
  workerSocket: void 0
@@ -3024,10 +3091,15 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
3024
3091
  // -----------------------------------------------------------------------
3025
3092
  // Worker event wiring
3026
3093
  // -----------------------------------------------------------------------
3027
- /** Wires FILE_RECEIVED and ASSOCIATION_COMPLETE events on a worker. */
3094
+ /** Wires all events on a worker: file handling, association lifecycle, output capture. */
3028
3095
  wireWorkerEvents(worker) {
3029
3096
  this.wireFileReceived(worker);
3030
3097
  this.wireAssociationComplete(worker);
3098
+ this.wireAssociationReceived(worker);
3099
+ this.wireCStoreRequest(worker);
3100
+ this.wireEchoRequest(worker);
3101
+ this.wireRefusingAssociation(worker);
3102
+ this.wireOutputCapture(worker);
3031
3103
  }
3032
3104
  /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
3033
3105
  wireFileReceived(worker) {
@@ -3075,6 +3147,7 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
3075
3147
  const assocId = worker.associationId ?? data.associationId;
3076
3148
  const assocDir = worker.associationDir ?? "";
3077
3149
  const files = [...worker.files];
3150
+ const output = [...worker.outputLines];
3078
3151
  const endAt = Date.now();
3079
3152
  const startAt = worker.startAt ?? endAt;
3080
3153
  const totalBytes = sumArray(worker.fileSizes);
@@ -3092,19 +3165,65 @@ var DicomReceiver = class _DicomReceiver extends EventEmitter {
3092
3165
  totalBytes,
3093
3166
  bytesPerSecond,
3094
3167
  startAt,
3095
- endAt
3168
+ endAt,
3169
+ output
3096
3170
  });
3097
3171
  worker.state = "idle";
3098
3172
  worker.associationId = void 0;
3099
3173
  worker.associationDir = void 0;
3100
3174
  worker.files = [];
3101
3175
  worker.fileSizes = [];
3176
+ worker.outputLines = [];
3102
3177
  worker.startAt = void 0;
3103
3178
  worker.remoteSocket = void 0;
3104
3179
  worker.workerSocket = void 0;
3105
3180
  void this.scaleDown();
3106
3181
  });
3107
3182
  }
3183
+ /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
3184
+ wireAssociationReceived(worker) {
3185
+ worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
3186
+ this.emit("ASSOCIATION_RECEIVED", {
3187
+ associationId: worker.associationId ?? "",
3188
+ callingAE: data.callingAE,
3189
+ calledAE: data.calledAE,
3190
+ source: data.source
3191
+ });
3192
+ });
3193
+ }
3194
+ /** Bubbles C_STORE_REQUEST from dcmrecv worker. */
3195
+ wireCStoreRequest(worker) {
3196
+ worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
3197
+ this.emit("C_STORE_REQUEST", {
3198
+ associationId: worker.associationId ?? "",
3199
+ raw: data.raw
3200
+ });
3201
+ });
3202
+ }
3203
+ /** Bubbles ECHO_REQUEST from dcmrecv worker. */
3204
+ wireEchoRequest(worker) {
3205
+ worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
3206
+ this.emit("ECHO_REQUEST", {
3207
+ associationId: worker.associationId ?? ""
3208
+ });
3209
+ });
3210
+ }
3211
+ /** Bubbles REFUSING_ASSOCIATION from dcmrecv worker. */
3212
+ wireRefusingAssociation(worker) {
3213
+ worker.dcmrecv.onEvent("REFUSING_ASSOCIATION", (data) => {
3214
+ this.emit("REFUSING_ASSOCIATION", {
3215
+ reason: data.reason
3216
+ });
3217
+ });
3218
+ }
3219
+ /** Captures worker output lines during busy associations. */
3220
+ wireOutputCapture(worker) {
3221
+ worker.dcmrecv.on("line", ({ text }) => {
3222
+ if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
3223
+ worker.outputLines.push(text);
3224
+ }
3225
+ });
3226
+ }
3108
3227
  // -----------------------------------------------------------------------
3109
3228
  // Abort signal
3110
3229
  // -----------------------------------------------------------------------