@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.cjs CHANGED
@@ -2701,8 +2701,9 @@ var DEFAULT_MAX_POOL_SIZE = 10;
2701
2701
  var DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
2702
2702
  var CONNECTION_RETRY_INTERVAL_MS = 500;
2703
2703
  var MAX_CONNECTION_RETRIES = 200;
2704
+ var MAX_OUTPUT_LINES_PER_ASSOCIATION = 500;
2704
2705
  var DicomReceiverOptionsSchema = zod.z.object({
2705
- port: zod.z.number().int().min(1).max(65535),
2706
+ port: zod.z.number().int().min(0).max(65535),
2706
2707
  storageDir: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in storageDir" }),
2707
2708
  aeTitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
2708
2709
  minPoolSize: zod.z.number().int().min(1).max(100).optional(),
@@ -2710,6 +2711,9 @@ var DicomReceiverOptionsSchema = zod.z.object({
2710
2711
  connectionTimeoutMs: zod.z.number().int().positive().optional(),
2711
2712
  configFile: zod.z.string().min(1).refine(isSafePath, { message: "path traversal detected in configFile" }).optional(),
2712
2713
  configProfile: zod.z.string().min(1).optional(),
2714
+ acseTimeout: zod.z.number().int().positive().optional(),
2715
+ dimseTimeout: zod.z.number().int().positive().optional(),
2716
+ maxPdu: zod.z.number().int().min(4096).max(131072).optional(),
2713
2717
  signal: zod.z.instanceof(AbortSignal).optional()
2714
2718
  }).strict().refine((data) => (data.minPoolSize ?? DEFAULT_MIN_POOL_SIZE) <= (data.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE), {
2715
2719
  message: "minPoolSize must be <= maxPoolSize"
@@ -2838,6 +2842,57 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2838
2842
  onAssociationComplete(listener) {
2839
2843
  return this.on("ASSOCIATION_COMPLETE", listener);
2840
2844
  }
2845
+ /**
2846
+ * Registers a listener for new associations (bubbled from workers).
2847
+ *
2848
+ * @param listener - Callback receiving association received data
2849
+ * @returns this for chaining
2850
+ */
2851
+ onAssociationReceived(listener) {
2852
+ return this.on("ASSOCIATION_RECEIVED", listener);
2853
+ }
2854
+ /**
2855
+ * Registers a listener for C-STORE requests (bubbled from workers).
2856
+ *
2857
+ * @param listener - Callback receiving C-STORE request data
2858
+ * @returns this for chaining
2859
+ */
2860
+ onCStoreRequest(listener) {
2861
+ return this.on("C_STORE_REQUEST", listener);
2862
+ }
2863
+ /**
2864
+ * Registers a listener for C-ECHO requests (bubbled from workers).
2865
+ *
2866
+ * @param listener - Callback receiving echo request data
2867
+ * @returns this for chaining
2868
+ */
2869
+ onEchoRequest(listener) {
2870
+ return this.on("ECHO_REQUEST", listener);
2871
+ }
2872
+ /**
2873
+ * Registers a listener for refused associations (bubbled from workers).
2874
+ *
2875
+ * @param listener - Callback receiving refusing association data
2876
+ * @returns this for chaining
2877
+ */
2878
+ onRefusingAssociation(listener) {
2879
+ return this.on("REFUSING_ASSOCIATION", listener);
2880
+ }
2881
+ /**
2882
+ * Routes an external socket to an idle worker.
2883
+ *
2884
+ * The pool must be started via `start()` before calling this method.
2885
+ * Use this when managing your own TCP listener (e.g., protocol router).
2886
+ *
2887
+ * @param socket - An incoming net.Socket to route to a worker
2888
+ */
2889
+ handleSocket(socket) {
2890
+ if (!this.started) {
2891
+ socket.destroy(new Error("DicomReceiver: not started"));
2892
+ return;
2893
+ }
2894
+ void this.handleConnection(socket);
2895
+ }
2841
2896
  /** Current pool status. */
2842
2897
  get poolStatus() {
2843
2898
  let idle = 0;
@@ -2851,8 +2906,11 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2851
2906
  // -----------------------------------------------------------------------
2852
2907
  // TCP proxy
2853
2908
  // -----------------------------------------------------------------------
2854
- /** Starts the TCP proxy on the configured port. */
2909
+ /** Starts the TCP proxy on the configured port. Skips if port is 0. */
2855
2910
  startTcpProxy() {
2911
+ if (this.options.port === 0) {
2912
+ return Promise.resolve(ok(void 0));
2913
+ }
2856
2914
  return new Promise((resolve) => {
2857
2915
  this.tcpServer = net__namespace.createServer((socket) => {
2858
2916
  void this.handleConnection(socket);
@@ -2905,6 +2963,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2905
2963
  worker.associationDir = associationDir;
2906
2964
  worker.files = [];
2907
2965
  worker.fileSizes = [];
2966
+ worker.outputLines = [];
2908
2967
  worker.startAt = Date.now();
2909
2968
  this.pipeConnection(worker, remoteSocket);
2910
2969
  void this.replenishPool();
@@ -2963,6 +3022,19 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2963
3022
  }
2964
3023
  return ok(void 0);
2965
3024
  }
3025
+ /** Creates a Dcmrecv instance with the pool's shared options. */
3026
+ createDcmrecv(port, tempDir) {
3027
+ return Dcmrecv.create({
3028
+ port,
3029
+ aeTitle: this.options.aeTitle ?? "DCMRECV",
3030
+ outputDirectory: tempDir,
3031
+ configFile: this.options.configFile,
3032
+ configProfile: this.options.configProfile,
3033
+ acseTimeout: this.options.acseTimeout,
3034
+ dimseTimeout: this.options.dimseTimeout,
3035
+ maxPdu: this.options.maxPdu
3036
+ });
3037
+ }
2966
3038
  /** Spawns a single Dcmrecv worker with an ephemeral port. */
2967
3039
  async spawnWorker() {
2968
3040
  const portResult = await allocatePort();
@@ -2971,13 +3043,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2971
3043
  const tempDir = path__namespace.join(os__namespace.tmpdir(), `dcmrecv-pool-${String(port)}-${String(Date.now())}`);
2972
3044
  const mkdirResult = await ensureDirectory(tempDir);
2973
3045
  if (!mkdirResult.ok) return mkdirResult;
2974
- const createResult = Dcmrecv.create({
2975
- port,
2976
- aeTitle: this.options.aeTitle ?? "DCMRECV",
2977
- outputDirectory: tempDir,
2978
- configFile: this.options.configFile,
2979
- configProfile: this.options.configProfile
2980
- });
3046
+ const createResult = this.createDcmrecv(port, tempDir);
2981
3047
  if (!createResult.ok) return createResult;
2982
3048
  const dcmrecv = createResult.value;
2983
3049
  const worker = {
@@ -2989,6 +3055,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
2989
3055
  associationDir: void 0,
2990
3056
  files: [],
2991
3057
  fileSizes: [],
3058
+ outputLines: [],
2992
3059
  startAt: void 0,
2993
3060
  remoteSocket: void 0,
2994
3061
  workerSocket: void 0
@@ -3050,10 +3117,15 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3050
3117
  // -----------------------------------------------------------------------
3051
3118
  // Worker event wiring
3052
3119
  // -----------------------------------------------------------------------
3053
- /** Wires FILE_RECEIVED and ASSOCIATION_COMPLETE events on a worker. */
3120
+ /** Wires all events on a worker: file handling, association lifecycle, output capture. */
3054
3121
  wireWorkerEvents(worker) {
3055
3122
  this.wireFileReceived(worker);
3056
3123
  this.wireAssociationComplete(worker);
3124
+ this.wireAssociationReceived(worker);
3125
+ this.wireCStoreRequest(worker);
3126
+ this.wireEchoRequest(worker);
3127
+ this.wireRefusingAssociation(worker);
3128
+ this.wireOutputCapture(worker);
3057
3129
  }
3058
3130
  /** Wires FILE_RECEIVED from dcmrecv worker to handleFileReceived. */
3059
3131
  wireFileReceived(worker) {
@@ -3101,6 +3173,7 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3101
3173
  const assocId = worker.associationId ?? data.associationId;
3102
3174
  const assocDir = worker.associationDir ?? "";
3103
3175
  const files = [...worker.files];
3176
+ const output = [...worker.outputLines];
3104
3177
  const endAt = Date.now();
3105
3178
  const startAt = worker.startAt ?? endAt;
3106
3179
  const totalBytes = sumArray(worker.fileSizes);
@@ -3118,19 +3191,65 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3118
3191
  totalBytes,
3119
3192
  bytesPerSecond,
3120
3193
  startAt,
3121
- endAt
3194
+ endAt,
3195
+ output
3122
3196
  });
3123
3197
  worker.state = "idle";
3124
3198
  worker.associationId = void 0;
3125
3199
  worker.associationDir = void 0;
3126
3200
  worker.files = [];
3127
3201
  worker.fileSizes = [];
3202
+ worker.outputLines = [];
3128
3203
  worker.startAt = void 0;
3129
3204
  worker.remoteSocket = void 0;
3130
3205
  worker.workerSocket = void 0;
3131
3206
  void this.scaleDown();
3132
3207
  });
3133
3208
  }
3209
+ /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
3210
+ wireAssociationReceived(worker) {
3211
+ worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {
3212
+ this.emit("ASSOCIATION_RECEIVED", {
3213
+ associationId: worker.associationId ?? "",
3214
+ callingAE: data.callingAE,
3215
+ calledAE: data.calledAE,
3216
+ source: data.source
3217
+ });
3218
+ });
3219
+ }
3220
+ /** Bubbles C_STORE_REQUEST from dcmrecv worker. */
3221
+ wireCStoreRequest(worker) {
3222
+ worker.dcmrecv.onEvent("C_STORE_REQUEST", (data) => {
3223
+ this.emit("C_STORE_REQUEST", {
3224
+ associationId: worker.associationId ?? "",
3225
+ raw: data.raw
3226
+ });
3227
+ });
3228
+ }
3229
+ /** Bubbles ECHO_REQUEST from dcmrecv worker. */
3230
+ wireEchoRequest(worker) {
3231
+ worker.dcmrecv.onEvent("ECHO_REQUEST", () => {
3232
+ this.emit("ECHO_REQUEST", {
3233
+ associationId: worker.associationId ?? ""
3234
+ });
3235
+ });
3236
+ }
3237
+ /** Bubbles REFUSING_ASSOCIATION from dcmrecv worker. */
3238
+ wireRefusingAssociation(worker) {
3239
+ worker.dcmrecv.onEvent("REFUSING_ASSOCIATION", (data) => {
3240
+ this.emit("REFUSING_ASSOCIATION", {
3241
+ reason: data.reason
3242
+ });
3243
+ });
3244
+ }
3245
+ /** Captures worker output lines during busy associations. */
3246
+ wireOutputCapture(worker) {
3247
+ worker.dcmrecv.on("line", ({ text }) => {
3248
+ if (worker.state === "busy" && worker.outputLines.length < MAX_OUTPUT_LINES_PER_ASSOCIATION) {
3249
+ worker.outputLines.push(text);
3250
+ }
3251
+ });
3252
+ }
3134
3253
  // -----------------------------------------------------------------------
3135
3254
  // Abort signal
3136
3255
  // -----------------------------------------------------------------------