@ubercode/dcmtk 0.7.2 → 0.8.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
@@ -33106,6 +33106,30 @@ async function echoscu(options) {
33106
33106
  return ok({ success: true, stderr: result.value.stderr });
33107
33107
  }
33108
33108
  var VERBOSITY_FLAGS19 = { verbose: "-v", debug: "-d" };
33109
+ var DECOMPRESS_FLAGS = {
33110
+ never: "--decompress-never",
33111
+ lossless: "--decompress-lossless",
33112
+ lossy: "--decompress-lossy"
33113
+ };
33114
+ function pushSendModeArgs(args, options) {
33115
+ if (options.noHalt === true) {
33116
+ args.push("--no-halt");
33117
+ }
33118
+ if (options.noIllegalProposal === true) {
33119
+ args.push("--no-illegal-proposal");
33120
+ }
33121
+ if (options.decompress !== void 0) {
33122
+ args.push(DECOMPRESS_FLAGS[options.decompress]);
33123
+ }
33124
+ if (options.multiAssociations === true) {
33125
+ args.push("+ma");
33126
+ } else if (options.multiAssociations === false) {
33127
+ args.push("-ma");
33128
+ }
33129
+ if (options.createReportFile !== void 0) {
33130
+ args.push("--create-report-file", options.createReportFile);
33131
+ }
33132
+ }
33109
33133
  var DcmsendOptionsSchema = z.object({
33110
33134
  timeoutMs: z.number().int().positive().optional(),
33111
33135
  signal: z.instanceof(AbortSignal).optional(),
@@ -33115,8 +33139,15 @@ var DcmsendOptionsSchema = z.object({
33115
33139
  callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
33116
33140
  calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
33117
33141
  scanDirectory: z.boolean().optional(),
33142
+ recurse: z.boolean().optional(),
33143
+ scanPattern: z.string().min(1).optional(),
33118
33144
  verbosity: z.enum(["verbose", "debug"]).optional(),
33119
33145
  noUidChecks: z.boolean().optional(),
33146
+ noHalt: z.boolean().optional(),
33147
+ noIllegalProposal: z.boolean().optional(),
33148
+ decompress: z.enum(["never", "lossless", "lossy"]).optional(),
33149
+ multiAssociations: z.boolean().optional(),
33150
+ createReportFile: z.string().min(1).optional(),
33120
33151
  maxPduReceive: z.number().int().min(4096).max(131072).optional(),
33121
33152
  maxPduSend: z.number().int().min(4096).max(131072).optional(),
33122
33153
  noHostnameLookup: z.boolean().optional(),
@@ -33162,6 +33193,13 @@ function buildArgs33(options) {
33162
33193
  if (options.scanDirectory === true) {
33163
33194
  args.push("--scan-directories");
33164
33195
  }
33196
+ if (options.recurse === true) {
33197
+ args.push("+r");
33198
+ }
33199
+ if (options.scanPattern !== void 0) {
33200
+ args.push("--scan-pattern", options.scanPattern);
33201
+ }
33202
+ pushSendModeArgs(args, options);
33165
33203
  args.push(options.host, String(options.port));
33166
33204
  args.push(...options.files);
33167
33205
  return args;
@@ -33200,7 +33238,18 @@ var ProposedTransferSyntax = {
33200
33238
  J2K_LOSSLESS: "j2kLossless",
33201
33239
  J2K_LOSSY: "j2kLossy",
33202
33240
  JLS_LOSSLESS: "jlsLossless",
33203
- JLS_LOSSY: "jlsLossy"
33241
+ JLS_LOSSY: "jlsLossy",
33242
+ MPEG2: "mpeg2",
33243
+ MPEG2_HIGH: "mpeg2High",
33244
+ MPEG4: "mpeg4",
33245
+ MPEG4_BD: "mpeg4Bd",
33246
+ MPEG4_2_2D: "mpeg4_2_2d",
33247
+ MPEG4_2_3D: "mpeg4_2_3d",
33248
+ MPEG4_2_ST: "mpeg4_2_st",
33249
+ HEVC: "hevc",
33250
+ HEVC10: "hevc10",
33251
+ RLE: "rle",
33252
+ DEFLATED: "deflated"
33204
33253
  };
33205
33254
  var PROPOSED_TS_FLAG_MAP = {
33206
33255
  [ProposedTransferSyntax.UNCOMPRESSED]: "-x=",
@@ -33213,8 +33262,20 @@ var PROPOSED_TS_FLAG_MAP = {
33213
33262
  [ProposedTransferSyntax.J2K_LOSSLESS]: "-xv",
33214
33263
  [ProposedTransferSyntax.J2K_LOSSY]: "-xw",
33215
33264
  [ProposedTransferSyntax.JLS_LOSSLESS]: "-xt",
33216
- [ProposedTransferSyntax.JLS_LOSSY]: "-xu"
33265
+ [ProposedTransferSyntax.JLS_LOSSY]: "-xu",
33266
+ [ProposedTransferSyntax.MPEG2]: "-xm",
33267
+ [ProposedTransferSyntax.MPEG2_HIGH]: "-xh",
33268
+ [ProposedTransferSyntax.MPEG4]: "-xn",
33269
+ [ProposedTransferSyntax.MPEG4_BD]: "-xl",
33270
+ [ProposedTransferSyntax.MPEG4_2_2D]: "-x2",
33271
+ [ProposedTransferSyntax.MPEG4_2_3D]: "-x3",
33272
+ [ProposedTransferSyntax.MPEG4_2_ST]: "-xo",
33273
+ [ProposedTransferSyntax.HEVC]: "-x4",
33274
+ [ProposedTransferSyntax.HEVC10]: "-x5",
33275
+ [ProposedTransferSyntax.RLE]: "-xr",
33276
+ [ProposedTransferSyntax.DEFLATED]: "-xd"
33217
33277
  };
33278
+ var PROPOSED_TS_VALUES = Object.values(ProposedTransferSyntax);
33218
33279
  var StorescuOptionsSchema = z.object({
33219
33280
  timeoutMs: z.number().int().positive().optional(),
33220
33281
  signal: z.instanceof(AbortSignal).optional(),
@@ -33233,20 +33294,18 @@ var StorescuOptionsSchema = z.object({
33233
33294
  dimseTimeout: z.number().int().positive().optional(),
33234
33295
  noHostnameLookup: z.boolean().optional(),
33235
33296
  required: z.boolean().optional(),
33236
- proposedTransferSyntax: z.enum([
33237
- "uncompressed",
33238
- "littleEndian",
33239
- "bigEndian",
33240
- "implicitVR",
33241
- "jpegLossless",
33242
- "jpeg8Bit",
33243
- "jpeg12Bit",
33244
- "j2kLossless",
33245
- "j2kLossy",
33246
- "jlsLossless",
33247
- "jlsLossy"
33248
- ]).optional()
33297
+ proposedTransferSyntax: z.union([z.enum(PROPOSED_TS_VALUES), z.array(z.enum(PROPOSED_TS_VALUES)).min(1)]).optional(),
33298
+ combineProposedTransferSyntaxes: z.boolean().optional()
33249
33299
  }).strict();
33300
+ function pushProposedTsArgs(args, ts) {
33301
+ if (typeof ts === "string") {
33302
+ args.push(PROPOSED_TS_FLAG_MAP[ts]);
33303
+ } else {
33304
+ for (let i = 0; i < ts.length; i++) {
33305
+ args.push(PROPOSED_TS_FLAG_MAP[ts[i]]);
33306
+ }
33307
+ }
33308
+ }
33250
33309
  var VERBOSITY_FLAGS20 = { verbose: "-v", debug: "-d" };
33251
33310
  var DIMSE_ERROR_PATTERN = /^E:.*(?:DIMSE Failed|Store Failed)/m;
33252
33311
  function pushNetworkArgs3(args, options) {
@@ -33288,7 +33347,10 @@ function buildArgs34(options) {
33288
33347
  args.push("+r");
33289
33348
  }
33290
33349
  if (options.proposedTransferSyntax !== void 0) {
33291
- args.push(PROPOSED_TS_FLAG_MAP[options.proposedTransferSyntax]);
33350
+ pushProposedTsArgs(args, options.proposedTransferSyntax);
33351
+ }
33352
+ if (options.combineProposedTransferSyntaxes === true) {
33353
+ args.push("+C");
33292
33354
  }
33293
33355
  if (options.required === true) {
33294
33356
  args.push("-R");
@@ -36828,79 +36890,13 @@ var SenderHealth = {
36828
36890
  DOWN: "down"
36829
36891
  };
36830
36892
 
36831
- // src/senders/DicomSender.ts
36832
- var DEFAULT_MAX_ASSOCIATIONS = 4;
36833
- var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
36834
- var DEFAULT_MAX_RETRIES = 3;
36835
- var DEFAULT_RETRY_DELAY_MS = 1e3;
36836
- var DEFAULT_BUCKET_FLUSH_MS = 5e3;
36837
- var DEFAULT_MAX_BUCKET_SIZE = 50;
36838
- var MAX_ASSOCIATIONS_LIMIT = 64;
36893
+ // src/senders/SenderEngine.ts
36839
36894
  var DEGRADE_THRESHOLD = 3;
36840
36895
  var DOWN_THRESHOLD = 10;
36841
36896
  var RECOVERY_THRESHOLD = 3;
36842
- var DicomSenderOptionsSchema = z.object({
36843
- host: z.string().min(1),
36844
- port: z.number().int().min(1).max(65535),
36845
- calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
36846
- callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
36847
- mode: z.enum(["single", "multiple", "bucket"]).optional(),
36848
- maxAssociations: z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
36849
- proposedTransferSyntax: z.enum([
36850
- "uncompressed",
36851
- "littleEndian",
36852
- "bigEndian",
36853
- "implicitVR",
36854
- "jpegLossless",
36855
- "jpeg8Bit",
36856
- "jpeg12Bit",
36857
- "j2kLossless",
36858
- "j2kLossy",
36859
- "jlsLossless",
36860
- "jlsLossy"
36861
- ]).optional(),
36862
- maxQueueLength: z.number().int().min(1).optional(),
36863
- timeoutMs: z.number().int().positive().optional(),
36864
- maxRetries: z.number().int().min(0).optional(),
36865
- retryDelayMs: z.number().int().min(0).optional(),
36866
- bucketFlushMs: z.number().int().positive().optional(),
36867
- maxBucketSize: z.number().int().min(1).optional(),
36868
- maxPduReceive: z.number().int().min(4096).max(131072).optional(),
36869
- maxPduSend: z.number().int().min(4096).max(131072).optional(),
36870
- associationTimeout: z.number().int().positive().optional(),
36871
- acseTimeout: z.number().int().positive().optional(),
36872
- dimseTimeout: z.number().int().positive().optional(),
36873
- noHostnameLookup: z.boolean().optional(),
36874
- verbosity: z.enum(["verbose", "debug"]).optional(),
36875
- required: z.boolean().optional(),
36876
- signal: z.instanceof(AbortSignal).optional()
36877
- }).strict();
36878
- function resolveConfig(options) {
36879
- const mode = options.mode ?? "multiple";
36880
- const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
36881
- return {
36882
- mode,
36883
- configuredMaxAssociations: mode === "single" ? 1 : rawMax,
36884
- maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
36885
- defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
36886
- defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
36887
- retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
36888
- bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
36889
- maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
36890
- };
36891
- }
36892
- var DicomSender = class _DicomSender extends EventEmitter {
36893
- constructor(options) {
36894
- super();
36895
- __publicField(this, "options");
36896
- __publicField(this, "mode");
36897
- __publicField(this, "configuredMaxAssociations");
36898
- __publicField(this, "maxQueueLength");
36899
- __publicField(this, "defaultTimeoutMs");
36900
- __publicField(this, "defaultMaxRetries");
36901
- __publicField(this, "retryDelayMs");
36902
- __publicField(this, "bucketFlushMs");
36903
- __publicField(this, "maxBucketSize");
36897
+ var SenderEngine = class {
36898
+ constructor(config) {
36899
+ __publicField(this, "config");
36904
36900
  // Queue and concurrency state
36905
36901
  __publicField(this, "queue", []);
36906
36902
  __publicField(this, "activeAssociations", 0);
@@ -36913,108 +36909,36 @@ var DicomSender = class _DicomSender extends EventEmitter {
36913
36909
  // Bucket state (bucket mode only)
36914
36910
  __publicField(this, "currentBucket", []);
36915
36911
  __publicField(this, "bucketTimer");
36916
- // AbortSignal
36917
- __publicField(this, "abortHandler");
36918
- this.setMaxListeners(20);
36919
- this.on("error", () => {
36920
- });
36921
- this.options = options;
36922
- const cfg = resolveConfig(options);
36923
- this.mode = cfg.mode;
36924
- this.configuredMaxAssociations = cfg.configuredMaxAssociations;
36925
- this.effectiveMaxAssociations = cfg.configuredMaxAssociations;
36926
- this.maxQueueLength = cfg.maxQueueLength;
36927
- this.defaultTimeoutMs = cfg.defaultTimeoutMs;
36928
- this.defaultMaxRetries = cfg.defaultMaxRetries;
36929
- this.retryDelayMs = cfg.retryDelayMs;
36930
- this.bucketFlushMs = cfg.bucketFlushMs;
36931
- this.maxBucketSize = cfg.maxBucketSize;
36932
- if (options.signal !== void 0) {
36933
- this.wireAbortSignal(options.signal);
36934
- }
36912
+ this.config = config;
36913
+ this.effectiveMaxAssociations = config.configuredMaxAssociations;
36935
36914
  }
36936
36915
  // -----------------------------------------------------------------------
36937
36916
  // Public API
36938
36917
  // -----------------------------------------------------------------------
36939
- /**
36940
- * Creates a new DicomSender instance.
36941
- *
36942
- * @param options - Configuration options
36943
- * @returns A Result containing the instance or a validation error
36944
- */
36945
- static create(options) {
36946
- const validation = DicomSenderOptionsSchema.safeParse(options);
36947
- if (!validation.success) {
36948
- return err(createValidationError("DicomSender", validation.error));
36949
- }
36950
- return ok(new _DicomSender(options));
36951
- }
36952
- /**
36953
- * Sends one or more DICOM files to the remote endpoint.
36954
- *
36955
- * In single/multiple mode, files are sent as one storescu call.
36956
- * In bucket mode, files are accumulated into a bucket and flushed
36957
- * when the bucket reaches maxBucketSize or the flush timer fires.
36958
- *
36959
- * The returned promise resolves when the files are actually sent
36960
- * (not just queued). Callers can await for confirmation or
36961
- * fire-and-forget with `void sender.send(files)`.
36962
- *
36963
- * @param files - One or more DICOM file paths
36964
- * @param options - Per-send overrides
36965
- * @returns A Result containing the send result or an error
36966
- */
36967
- send(files, options) {
36918
+ /** Sends files through the engine's queue/bucket system. */
36919
+ send(files, timeoutMs, maxRetries, binaryParams) {
36968
36920
  if (this.isStopped) {
36969
- return Promise.resolve(err(new Error("DicomSender: sender is stopped")));
36921
+ return Promise.resolve(err(new Error(`${this.config.senderName}: sender is stopped`)));
36970
36922
  }
36971
36923
  if (files.length === 0) {
36972
- return Promise.resolve(err(new Error("DicomSender: no files provided")));
36973
- }
36974
- const params = {
36975
- timeoutMs: options?.timeoutMs ?? this.defaultTimeoutMs,
36976
- maxRetries: options?.maxRetries ?? this.defaultMaxRetries,
36977
- calledAETitle: options?.calledAETitle,
36978
- callingAETitle: options?.callingAETitle,
36979
- required: options?.required
36980
- };
36981
- return this.dispatchSend(files, params);
36982
- }
36983
- /** Dispatches a send to the appropriate mode handler. */
36984
- dispatchSend(files, params) {
36985
- switch (this.mode) {
36986
- case "single":
36987
- case "multiple":
36988
- return this.enqueueSend(files, params);
36989
- case "bucket":
36990
- return this.enqueueBucket(files, params);
36991
- default:
36992
- assertUnreachable(this.mode);
36924
+ return Promise.resolve(err(new Error(`${this.config.senderName}: no files provided`)));
36993
36925
  }
36926
+ return this.dispatchSend(files, timeoutMs, maxRetries, binaryParams);
36994
36927
  }
36995
- /**
36996
- * Flushes the current bucket immediately (bucket mode only).
36997
- * In single/multiple mode this is a no-op.
36998
- */
36928
+ /** Flushes the current bucket immediately (bucket mode only). */
36999
36929
  flush() {
37000
- if (this.mode !== "bucket") return;
36930
+ if (this.config.mode !== "bucket") return;
37001
36931
  if (this.currentBucket.length === 0) return;
37002
36932
  this.clearBucketTimer();
37003
36933
  this.flushBucketInternal("timer");
37004
36934
  }
37005
- /**
37006
- * Gracefully stops the sender. Rejects all queued items and
37007
- * waits for active associations to complete.
37008
- */
36935
+ /** Gracefully stops the engine. Rejects all queued items and waits for active associations. */
37009
36936
  async stop() {
37010
36937
  if (this.isStopped) return;
37011
36938
  this.isStopped = true;
37012
- if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
37013
- this.options.signal.removeEventListener("abort", this.abortHandler);
37014
- }
37015
36939
  this.clearBucketTimer();
37016
- this.rejectBucket("DicomSender: sender stopped");
37017
- this.rejectQueue("DicomSender: sender stopped");
36940
+ this.rejectBucket(`${this.config.senderName}: sender stopped`);
36941
+ this.rejectQueue(`${this.config.senderName}: sender stopped`);
37018
36942
  await this.waitForActive();
37019
36943
  }
37020
36944
  /** Current sender status. */
@@ -37029,75 +36953,37 @@ var DicomSender = class _DicomSender extends EventEmitter {
37029
36953
  stopped: this.isStopped
37030
36954
  };
37031
36955
  }
36956
+ /** Whether the engine has been stopped. */
36957
+ get stopped() {
36958
+ return this.isStopped;
36959
+ }
37032
36960
  // -----------------------------------------------------------------------
37033
- // Typed event listener convenience methods
36961
+ // Dispatch
37034
36962
  // -----------------------------------------------------------------------
37035
- /**
37036
- * Registers a typed listener for a DicomSender-specific event.
37037
- *
37038
- * @param event - The event name from DicomSenderEventMap
37039
- * @param listener - Callback receiving typed event data
37040
- * @returns this for chaining
37041
- */
37042
- onEvent(event, listener) {
37043
- return this.on(event, listener);
37044
- }
37045
- /**
37046
- * Registers a listener for successful sends.
37047
- *
37048
- * @param listener - Callback receiving send complete data
37049
- * @returns this for chaining
37050
- */
37051
- onSendComplete(listener) {
37052
- return this.on("SEND_COMPLETE", listener);
37053
- }
37054
- /**
37055
- * Registers a listener for failed sends.
37056
- *
37057
- * @param listener - Callback receiving send failed data
37058
- * @returns this for chaining
37059
- */
37060
- onSendFailed(listener) {
37061
- return this.on("SEND_FAILED", listener);
37062
- }
37063
- /**
37064
- * Registers a listener for health state changes.
37065
- *
37066
- * @param listener - Callback receiving health change data
37067
- * @returns this for chaining
37068
- */
37069
- onHealthChanged(listener) {
37070
- return this.on("HEALTH_CHANGED", listener);
37071
- }
37072
- /**
37073
- * Registers a listener for bucket flushes (bucket mode only).
37074
- *
37075
- * @param listener - Callback receiving bucket flush data
37076
- * @returns this for chaining
37077
- */
37078
- onBucketFlushed(listener) {
37079
- return this.on("BUCKET_FLUSHED", listener);
36963
+ /** Dispatches a send to the appropriate mode handler. */
36964
+ dispatchSend(files, timeoutMs, maxRetries, binaryParams) {
36965
+ switch (this.config.mode) {
36966
+ case "single":
36967
+ case "multiple":
36968
+ return this.enqueueSend(files, timeoutMs, maxRetries, binaryParams);
36969
+ case "bucket":
36970
+ return this.enqueueBucket(files, timeoutMs, maxRetries, binaryParams);
36971
+ default:
36972
+ assertUnreachable(this.config.mode);
36973
+ }
37080
36974
  }
37081
36975
  // -----------------------------------------------------------------------
37082
36976
  // Single/Multiple mode: queue-based dispatch
37083
36977
  // -----------------------------------------------------------------------
37084
36978
  /** Enqueues a send and dispatches immediately if capacity allows. */
37085
- enqueueSend(files, params) {
36979
+ enqueueSend(files, timeoutMs, maxRetries, binaryParams) {
37086
36980
  return new Promise((resolve) => {
37087
36981
  const totalQueued = this.queue.length + this.currentBucket.length;
37088
- if (totalQueued >= this.maxQueueLength) {
37089
- resolve(err(new Error("DicomSender: queue full")));
36982
+ if (totalQueued >= this.config.maxQueueLength) {
36983
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37090
36984
  return;
37091
36985
  }
37092
- const entry = {
37093
- files,
37094
- timeoutMs: params.timeoutMs,
37095
- maxRetries: params.maxRetries,
37096
- calledAETitle: params.calledAETitle,
37097
- callingAETitle: params.callingAETitle,
37098
- required: params.required,
37099
- resolve
37100
- };
36986
+ const entry = { files, timeoutMs, maxRetries, binaryParams, resolve };
37101
36987
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37102
36988
  void this.executeEntry(entry);
37103
36989
  } else {
@@ -37117,17 +37003,16 @@ var DicomSender = class _DicomSender extends EventEmitter {
37117
37003
  // Bucket mode: accumulate-then-flush
37118
37004
  // -----------------------------------------------------------------------
37119
37005
  /** Adds files to the current bucket and triggers flush if full. */
37120
- enqueueBucket(files, params) {
37006
+ enqueueBucket(files, timeoutMs, maxRetries, binaryParams) {
37121
37007
  return new Promise((resolve) => {
37122
37008
  const totalQueued = this.queue.length + this.currentBucket.length;
37123
- if (totalQueued >= this.maxQueueLength) {
37124
- resolve(err(new Error("DicomSender: queue full")));
37009
+ if (totalQueued >= this.config.maxQueueLength) {
37010
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37125
37011
  return;
37126
37012
  }
37127
- const { timeoutMs, maxRetries, calledAETitle, callingAETitle, required } = params;
37128
- this.currentBucket.push({ files, resolve, timeoutMs, maxRetries, calledAETitle, callingAETitle, required });
37013
+ this.currentBucket.push({ files, resolve, timeoutMs, maxRetries, binaryParams });
37129
37014
  const totalFiles = this.countBucketFiles();
37130
- if (totalFiles >= this.maxBucketSize) {
37015
+ if (totalFiles >= this.config.maxBucketSize) {
37131
37016
  this.clearBucketTimer();
37132
37017
  void this.flushBucketInternal("maxSize");
37133
37018
  } else {
@@ -37159,9 +37044,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37159
37044
  files: allFiles,
37160
37045
  timeoutMs,
37161
37046
  maxRetries,
37162
- calledAETitle: entries[0]?.calledAETitle,
37163
- callingAETitle: entries[0]?.callingAETitle,
37164
- required: entries[0]?.required,
37047
+ binaryParams: entries[0].binaryParams,
37165
37048
  resolve: (result) => {
37166
37049
  for (let i = 0; i < entries.length; i++) {
37167
37050
  entries[i].resolve(result);
@@ -37175,7 +37058,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37175
37058
  const entries = [...this.currentBucket];
37176
37059
  this.currentBucket = [];
37177
37060
  const bucketEntry = this.mergeBucketEntries(entries);
37178
- this.emit("BUCKET_FLUSHED", { fileCount: bucketEntry.files.length, reason });
37061
+ this.config.emitters.emitBucketFlushed({ fileCount: bucketEntry.files.length, reason });
37179
37062
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37180
37063
  void this.executeEntry(bucketEntry);
37181
37064
  } else {
@@ -37188,7 +37071,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37188
37071
  this.bucketTimer = setTimeout(() => {
37189
37072
  this.bucketTimer = void 0;
37190
37073
  void this.flushBucketInternal("timer");
37191
- }, this.bucketFlushMs);
37074
+ }, this.config.bucketFlushMs);
37192
37075
  }
37193
37076
  /** Clears the bucket flush timer. */
37194
37077
  clearBucketTimer() {
@@ -37200,7 +37083,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37200
37083
  // -----------------------------------------------------------------------
37201
37084
  // Core send execution with retry
37202
37085
  // -----------------------------------------------------------------------
37203
- /** Executes a single queue entry: calls storescu with retry. */
37086
+ /** Executes a single queue entry: calls the binary with retry. */
37204
37087
  async executeEntry(entry) {
37205
37088
  this.activeAssociations++;
37206
37089
  const startMs = Date.now();
@@ -37209,7 +37092,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37209
37092
  if (failure === void 0) return;
37210
37093
  this.activeAssociations--;
37211
37094
  this.recordFailure();
37212
- this.emit("SEND_FAILED", {
37095
+ this.config.emitters.emitSendFailed({
37213
37096
  files: entry.files,
37214
37097
  error: failure.error,
37215
37098
  attempts: maxAttempts,
@@ -37219,48 +37102,27 @@ var DicomSender = class _DicomSender extends EventEmitter {
37219
37102
  entry.resolve(err(failure.error));
37220
37103
  this.drainQueue();
37221
37104
  }
37222
- /** Attempts storescu up to maxAttempts times. Returns undefined on success, or failure info. */
37105
+ /** Attempts the binary call up to maxAttempts times. Returns undefined on success. */
37223
37106
  async attemptSend(entry, maxAttempts, startMs) {
37224
37107
  let lastError;
37225
37108
  const lastOutput = { stdout: "", stderr: "" };
37226
37109
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
37227
37110
  if (this.isStopped) {
37228
37111
  this.activeAssociations--;
37229
- entry.resolve(err(new Error("DicomSender: sender stopped")));
37112
+ entry.resolve(err(new Error(`${this.config.senderName}: sender stopped`)));
37230
37113
  return void 0;
37231
37114
  }
37232
- const result = await this.callStorescu(entry);
37115
+ const result = await this.config.executor(entry.files, entry.timeoutMs, entry.binaryParams, this.config.signal);
37233
37116
  if (result.ok) {
37234
37117
  this.handleSendSuccess(entry, startMs, result.value);
37235
37118
  return void 0;
37236
37119
  }
37237
37120
  lastError = result.error;
37238
37121
  if (attempt < maxAttempts - 1) {
37239
- await delay2(this.retryDelayMs * (attempt + 1));
37122
+ await delay2(this.config.retryDelayMs * (attempt + 1));
37240
37123
  }
37241
37124
  }
37242
- return { error: lastError ?? new Error("DicomSender: send failed"), output: lastOutput };
37243
- }
37244
- /** Calls storescu with the configured options. */
37245
- callStorescu(entry) {
37246
- return storescu({
37247
- host: this.options.host,
37248
- port: this.options.port,
37249
- files: [...entry.files],
37250
- calledAETitle: entry.calledAETitle ?? this.options.calledAETitle,
37251
- callingAETitle: entry.callingAETitle ?? this.options.callingAETitle,
37252
- proposedTransferSyntax: this.options.proposedTransferSyntax,
37253
- maxPduReceive: this.options.maxPduReceive,
37254
- maxPduSend: this.options.maxPduSend,
37255
- associationTimeout: this.options.associationTimeout,
37256
- acseTimeout: this.options.acseTimeout,
37257
- dimseTimeout: this.options.dimseTimeout,
37258
- noHostnameLookup: this.options.noHostnameLookup,
37259
- verbosity: this.options.verbosity,
37260
- required: entry.required ?? this.options.required,
37261
- timeoutMs: entry.timeoutMs,
37262
- signal: this.options.signal
37263
- });
37125
+ return { error: lastError ?? new Error(`${this.config.senderName}: send failed`), output: lastOutput };
37264
37126
  }
37265
37127
  /** Handles a successful send: updates state, emits event, resolves promise. */
37266
37128
  handleSendSuccess(entry, startMs, output) {
@@ -37268,7 +37130,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37268
37130
  const durationMs = Date.now() - startMs;
37269
37131
  this.recordSuccess();
37270
37132
  const data = { files: entry.files, fileCount: entry.files.length, durationMs, stdout: output.stdout, stderr: output.stderr };
37271
- this.emit("SEND_COMPLETE", data);
37133
+ this.config.emitters.emitSendComplete(data);
37272
37134
  entry.resolve(ok(data));
37273
37135
  this.drainQueue();
37274
37136
  }
@@ -37286,8 +37148,8 @@ var DicomSender = class _DicomSender extends EventEmitter {
37286
37148
  if (this.health === SenderHealth.DOWN) {
37287
37149
  this.health = SenderHealth.DEGRADED;
37288
37150
  } else {
37289
- this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.configuredMaxAssociations);
37290
- if (this.effectiveMaxAssociations >= this.configuredMaxAssociations) {
37151
+ this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.config.configuredMaxAssociations);
37152
+ if (this.effectiveMaxAssociations >= this.config.configuredMaxAssociations) {
37291
37153
  this.health = SenderHealth.HEALTHY;
37292
37154
  }
37293
37155
  }
@@ -37307,22 +37169,26 @@ var DicomSender = class _DicomSender extends EventEmitter {
37307
37169
  this.emitHealthChanged(previousHealth);
37308
37170
  }
37309
37171
  } else if (this.consecutiveFailures >= DEGRADE_THRESHOLD && this.consecutiveFailures % DEGRADE_THRESHOLD === 0) {
37310
- if (this.health === SenderHealth.HEALTHY) {
37311
- this.health = SenderHealth.DEGRADED;
37312
- this.effectiveMaxAssociations = Math.max(1, Math.floor(this.configuredMaxAssociations / 2));
37172
+ this.degradeHealth(previousHealth);
37173
+ }
37174
+ }
37175
+ /** Handles HEALTHY→DEGRADED or DEGRADED→DEGRADED transitions. */
37176
+ degradeHealth(previousHealth) {
37177
+ if (this.health === SenderHealth.HEALTHY) {
37178
+ this.health = SenderHealth.DEGRADED;
37179
+ this.effectiveMaxAssociations = Math.max(1, Math.floor(this.config.configuredMaxAssociations / 2));
37180
+ this.emitHealthChanged(previousHealth);
37181
+ } else if (this.health === SenderHealth.DEGRADED) {
37182
+ const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
37183
+ if (newMax !== this.effectiveMaxAssociations) {
37184
+ this.effectiveMaxAssociations = newMax;
37313
37185
  this.emitHealthChanged(previousHealth);
37314
- } else if (this.health === SenderHealth.DEGRADED) {
37315
- const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
37316
- if (newMax !== this.effectiveMaxAssociations) {
37317
- this.effectiveMaxAssociations = newMax;
37318
- this.emitHealthChanged(previousHealth);
37319
- }
37320
37186
  }
37321
37187
  }
37322
37188
  }
37323
- /** Emits a HEALTH_CHANGED event. */
37189
+ /** Emits a HEALTH_CHANGED event via the wrapper's emitter. */
37324
37190
  emitHealthChanged(previousHealth) {
37325
- this.emit("HEALTH_CHANGED", {
37191
+ this.config.emitters.emitHealthChanged({
37326
37192
  previousHealth,
37327
37193
  newHealth: this.health,
37328
37194
  effectiveMaxAssociations: this.effectiveMaxAssociations,
@@ -37362,20 +37228,6 @@ var DicomSender = class _DicomSender extends EventEmitter {
37362
37228
  check();
37363
37229
  });
37364
37230
  }
37365
- // -----------------------------------------------------------------------
37366
- // Abort signal
37367
- // -----------------------------------------------------------------------
37368
- /** Wires an AbortSignal to stop the sender. */
37369
- wireAbortSignal(signal) {
37370
- if (signal.aborted) {
37371
- void this.stop();
37372
- return;
37373
- }
37374
- this.abortHandler = () => {
37375
- void this.stop();
37376
- };
37377
- signal.addEventListener("abort", this.abortHandler, { once: true });
37378
- }
37379
37231
  };
37380
37232
  function delay2(ms) {
37381
37233
  return new Promise((resolve) => {
@@ -37383,6 +37235,492 @@ function delay2(ms) {
37383
37235
  });
37384
37236
  }
37385
37237
 
37238
+ // src/senders/DicomSender.ts
37239
+ var DEFAULT_MAX_ASSOCIATIONS = 4;
37240
+ var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
37241
+ var DEFAULT_MAX_RETRIES = 3;
37242
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
37243
+ var DEFAULT_BUCKET_FLUSH_MS = 5e3;
37244
+ var DEFAULT_MAX_BUCKET_SIZE = 50;
37245
+ var MAX_ASSOCIATIONS_LIMIT = 64;
37246
+ var DicomSenderOptionsSchema = z.object({
37247
+ host: z.string().min(1),
37248
+ port: z.number().int().min(1).max(65535),
37249
+ calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37250
+ callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37251
+ mode: z.enum(["single", "multiple", "bucket"]).optional(),
37252
+ maxAssociations: z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
37253
+ proposedTransferSyntax: z.union([z.enum(PROPOSED_TS_VALUES), z.array(z.enum(PROPOSED_TS_VALUES)).min(1)]).optional(),
37254
+ combineProposedTransferSyntaxes: z.boolean().optional(),
37255
+ maxQueueLength: z.number().int().min(1).optional(),
37256
+ timeoutMs: z.number().int().positive().optional(),
37257
+ maxRetries: z.number().int().min(0).optional(),
37258
+ retryDelayMs: z.number().int().min(0).optional(),
37259
+ bucketFlushMs: z.number().int().positive().optional(),
37260
+ maxBucketSize: z.number().int().min(1).optional(),
37261
+ maxPduReceive: z.number().int().min(4096).max(131072).optional(),
37262
+ maxPduSend: z.number().int().min(4096).max(131072).optional(),
37263
+ associationTimeout: z.number().int().positive().optional(),
37264
+ acseTimeout: z.number().int().positive().optional(),
37265
+ dimseTimeout: z.number().int().positive().optional(),
37266
+ noHostnameLookup: z.boolean().optional(),
37267
+ verbosity: z.enum(["verbose", "debug"]).optional(),
37268
+ required: z.boolean().optional(),
37269
+ signal: z.instanceof(AbortSignal).optional()
37270
+ }).strict();
37271
+ function createStorescuExecutor(options) {
37272
+ return async (files, timeoutMs, params, signal) => {
37273
+ const result = await storescu({
37274
+ host: options.host,
37275
+ port: options.port,
37276
+ files: [...files],
37277
+ calledAETitle: params.calledAETitle ?? options.calledAETitle,
37278
+ callingAETitle: params.callingAETitle ?? options.callingAETitle,
37279
+ proposedTransferSyntax: params.proposedTransferSyntax ?? options.proposedTransferSyntax,
37280
+ combineProposedTransferSyntaxes: params.combineProposedTransferSyntaxes ?? options.combineProposedTransferSyntaxes,
37281
+ maxPduReceive: options.maxPduReceive,
37282
+ maxPduSend: options.maxPduSend,
37283
+ associationTimeout: options.associationTimeout,
37284
+ acseTimeout: options.acseTimeout,
37285
+ dimseTimeout: options.dimseTimeout,
37286
+ noHostnameLookup: options.noHostnameLookup,
37287
+ verbosity: options.verbosity,
37288
+ required: params.required ?? options.required,
37289
+ timeoutMs,
37290
+ signal
37291
+ });
37292
+ if (!result.ok) return err(result.error);
37293
+ return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37294
+ };
37295
+ }
37296
+ function resolveConfig(options) {
37297
+ const mode = options.mode ?? "multiple";
37298
+ const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
37299
+ return {
37300
+ mode,
37301
+ configuredMaxAssociations: mode === "single" ? 1 : rawMax,
37302
+ maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
37303
+ defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
37304
+ defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
37305
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
37306
+ bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
37307
+ maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
37308
+ };
37309
+ }
37310
+ var DicomSender = class _DicomSender extends EventEmitter {
37311
+ constructor(engine, cfg, signal) {
37312
+ super();
37313
+ __publicField(this, "engine");
37314
+ __publicField(this, "defaultTimeoutMs");
37315
+ __publicField(this, "defaultMaxRetries");
37316
+ __publicField(this, "signal");
37317
+ __publicField(this, "abortHandler");
37318
+ this.setMaxListeners(20);
37319
+ this.on("error", () => {
37320
+ });
37321
+ this.engine = engine;
37322
+ this.defaultTimeoutMs = cfg.defaultTimeoutMs;
37323
+ this.defaultMaxRetries = cfg.defaultMaxRetries;
37324
+ this.signal = signal;
37325
+ if (signal !== void 0) {
37326
+ this.wireAbortSignal(signal);
37327
+ }
37328
+ }
37329
+ // -----------------------------------------------------------------------
37330
+ // Public API
37331
+ // -----------------------------------------------------------------------
37332
+ /**
37333
+ * Creates a new DicomSender instance.
37334
+ *
37335
+ * @param options - Configuration options
37336
+ * @returns A Result containing the instance or a validation error
37337
+ */
37338
+ static create(options) {
37339
+ const validation = DicomSenderOptionsSchema.safeParse(options);
37340
+ if (!validation.success) {
37341
+ return err(createValidationError("DicomSender", validation.error));
37342
+ }
37343
+ const cfg = resolveConfig(options);
37344
+ const senderRef = { current: void 0 };
37345
+ const engine = new SenderEngine({
37346
+ ...cfg,
37347
+ executor: createStorescuExecutor(options),
37348
+ signal: options.signal,
37349
+ senderName: "DicomSender",
37350
+ emitters: {
37351
+ emitSendComplete: (data) => {
37352
+ senderRef.current.emit("SEND_COMPLETE", data);
37353
+ },
37354
+ emitSendFailed: (data) => {
37355
+ senderRef.current.emit("SEND_FAILED", data);
37356
+ },
37357
+ emitHealthChanged: (data) => {
37358
+ senderRef.current.emit("HEALTH_CHANGED", data);
37359
+ },
37360
+ emitBucketFlushed: (data) => {
37361
+ senderRef.current.emit("BUCKET_FLUSHED", data);
37362
+ }
37363
+ }
37364
+ });
37365
+ const sender = new _DicomSender(engine, cfg, options.signal);
37366
+ senderRef.current = sender;
37367
+ return ok(sender);
37368
+ }
37369
+ /**
37370
+ * Sends one or more DICOM files to the remote endpoint.
37371
+ *
37372
+ * In single/multiple mode, files are sent as one storescu call.
37373
+ * In bucket mode, files are accumulated into a bucket and flushed
37374
+ * when the bucket reaches maxBucketSize or the flush timer fires.
37375
+ *
37376
+ * The returned promise resolves when the files are actually sent
37377
+ * (not just queued). Callers can await for confirmation or
37378
+ * fire-and-forget with `void sender.send(files)`.
37379
+ *
37380
+ * @param files - One or more DICOM file paths
37381
+ * @param options - Per-send overrides
37382
+ * @returns A Result containing the send result or an error
37383
+ */
37384
+ send(files, options) {
37385
+ const params = buildStorescuParams(options);
37386
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
37387
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
37388
+ return this.engine.send(files, timeoutMs, maxRetries, params);
37389
+ }
37390
+ /**
37391
+ * Flushes the current bucket immediately (bucket mode only).
37392
+ * In single/multiple mode this is a no-op.
37393
+ */
37394
+ flush() {
37395
+ this.engine.flush();
37396
+ }
37397
+ /**
37398
+ * Gracefully stops the sender. Rejects all queued items and
37399
+ * waits for active associations to complete.
37400
+ */
37401
+ async stop() {
37402
+ if (this.signal !== void 0 && this.abortHandler !== void 0) {
37403
+ this.signal.removeEventListener("abort", this.abortHandler);
37404
+ }
37405
+ await this.engine.stop();
37406
+ }
37407
+ /** Current sender status. */
37408
+ get status() {
37409
+ return this.engine.status;
37410
+ }
37411
+ // -----------------------------------------------------------------------
37412
+ // Typed event listener convenience methods
37413
+ // -----------------------------------------------------------------------
37414
+ /**
37415
+ * Registers a typed listener for a DicomSender-specific event.
37416
+ *
37417
+ * @param event - The event name from DicomSenderEventMap
37418
+ * @param listener - Callback receiving typed event data
37419
+ * @returns this for chaining
37420
+ */
37421
+ onEvent(event, listener) {
37422
+ return this.on(event, listener);
37423
+ }
37424
+ /**
37425
+ * Registers a listener for successful sends.
37426
+ *
37427
+ * @param listener - Callback receiving send complete data
37428
+ * @returns this for chaining
37429
+ */
37430
+ onSendComplete(listener) {
37431
+ return this.on("SEND_COMPLETE", listener);
37432
+ }
37433
+ /**
37434
+ * Registers a listener for failed sends.
37435
+ *
37436
+ * @param listener - Callback receiving send failed data
37437
+ * @returns this for chaining
37438
+ */
37439
+ onSendFailed(listener) {
37440
+ return this.on("SEND_FAILED", listener);
37441
+ }
37442
+ /**
37443
+ * Registers a listener for health state changes.
37444
+ *
37445
+ * @param listener - Callback receiving health change data
37446
+ * @returns this for chaining
37447
+ */
37448
+ onHealthChanged(listener) {
37449
+ return this.on("HEALTH_CHANGED", listener);
37450
+ }
37451
+ /**
37452
+ * Registers a listener for bucket flushes (bucket mode only).
37453
+ *
37454
+ * @param listener - Callback receiving bucket flush data
37455
+ * @returns this for chaining
37456
+ */
37457
+ onBucketFlushed(listener) {
37458
+ return this.on("BUCKET_FLUSHED", listener);
37459
+ }
37460
+ // -----------------------------------------------------------------------
37461
+ // Abort signal
37462
+ // -----------------------------------------------------------------------
37463
+ /** Wires an AbortSignal to stop the sender. */
37464
+ wireAbortSignal(signal) {
37465
+ if (signal.aborted) {
37466
+ void this.stop();
37467
+ return;
37468
+ }
37469
+ this.abortHandler = () => {
37470
+ void this.stop();
37471
+ };
37472
+ signal.addEventListener("abort", this.abortHandler, { once: true });
37473
+ }
37474
+ };
37475
+ function buildStorescuParams(options) {
37476
+ return {
37477
+ calledAETitle: options?.calledAETitle,
37478
+ callingAETitle: options?.callingAETitle,
37479
+ required: options?.required,
37480
+ proposedTransferSyntax: options?.proposedTransferSyntax,
37481
+ combineProposedTransferSyntaxes: options?.combineProposedTransferSyntaxes
37482
+ };
37483
+ }
37484
+ var DEFAULT_MAX_ASSOCIATIONS2 = 4;
37485
+ var DEFAULT_MAX_QUEUE_LENGTH2 = 1e3;
37486
+ var DEFAULT_MAX_RETRIES2 = 3;
37487
+ var DEFAULT_RETRY_DELAY_MS2 = 1e3;
37488
+ var DEFAULT_BUCKET_FLUSH_MS2 = 5e3;
37489
+ var DEFAULT_MAX_BUCKET_SIZE2 = 50;
37490
+ var MAX_ASSOCIATIONS_LIMIT2 = 64;
37491
+ var DicomSendOptionsSchema = z.object({
37492
+ host: z.string().min(1),
37493
+ port: z.number().int().min(1).max(65535),
37494
+ calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37495
+ callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37496
+ mode: z.enum(["single", "multiple", "bucket"]).optional(),
37497
+ maxAssociations: z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT2).optional(),
37498
+ maxQueueLength: z.number().int().min(1).optional(),
37499
+ timeoutMs: z.number().int().positive().optional(),
37500
+ maxRetries: z.number().int().min(0).optional(),
37501
+ retryDelayMs: z.number().int().min(0).optional(),
37502
+ bucketFlushMs: z.number().int().positive().optional(),
37503
+ maxBucketSize: z.number().int().min(1).optional(),
37504
+ maxPduReceive: z.number().int().min(4096).max(131072).optional(),
37505
+ maxPduSend: z.number().int().min(4096).max(131072).optional(),
37506
+ associationTimeout: z.number().int().positive().optional(),
37507
+ acseTimeout: z.number().int().positive().optional(),
37508
+ dimseTimeout: z.number().int().positive().optional(),
37509
+ noHostnameLookup: z.boolean().optional(),
37510
+ verbosity: z.enum(["verbose", "debug"]).optional(),
37511
+ noHalt: z.boolean().optional(),
37512
+ noIllegalProposal: z.boolean().optional(),
37513
+ decompress: z.enum(["never", "lossless", "lossy"]).optional(),
37514
+ multiAssociations: z.boolean().optional(),
37515
+ noUidChecks: z.boolean().optional(),
37516
+ signal: z.instanceof(AbortSignal).optional()
37517
+ }).strict();
37518
+ function createDcmsendExecutor(options) {
37519
+ return async (files, timeoutMs, params, signal) => {
37520
+ const result = await dcmsend({
37521
+ host: options.host,
37522
+ port: options.port,
37523
+ files: [...files],
37524
+ calledAETitle: params.calledAETitle ?? options.calledAETitle,
37525
+ callingAETitle: params.callingAETitle ?? options.callingAETitle,
37526
+ maxPduReceive: options.maxPduReceive,
37527
+ maxPduSend: options.maxPduSend,
37528
+ associationTimeout: options.associationTimeout,
37529
+ acseTimeout: options.acseTimeout,
37530
+ dimseTimeout: options.dimseTimeout,
37531
+ noHostnameLookup: options.noHostnameLookup,
37532
+ verbosity: options.verbosity,
37533
+ noHalt: options.noHalt,
37534
+ noIllegalProposal: options.noIllegalProposal,
37535
+ decompress: options.decompress,
37536
+ multiAssociations: options.multiAssociations,
37537
+ noUidChecks: options.noUidChecks,
37538
+ timeoutMs,
37539
+ signal
37540
+ });
37541
+ if (!result.ok) return err(result.error);
37542
+ return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37543
+ };
37544
+ }
37545
+ function resolveConfig2(options) {
37546
+ const mode = options.mode ?? "multiple";
37547
+ const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS2;
37548
+ return {
37549
+ mode,
37550
+ configuredMaxAssociations: mode === "single" ? 1 : rawMax,
37551
+ maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH2,
37552
+ defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
37553
+ defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES2,
37554
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
37555
+ bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS2,
37556
+ maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE2
37557
+ };
37558
+ }
37559
+ var DicomSend = class _DicomSend extends EventEmitter {
37560
+ constructor(engine, cfg, signal) {
37561
+ super();
37562
+ __publicField(this, "engine");
37563
+ __publicField(this, "defaultTimeoutMs");
37564
+ __publicField(this, "defaultMaxRetries");
37565
+ __publicField(this, "signal");
37566
+ __publicField(this, "abortHandler");
37567
+ this.setMaxListeners(20);
37568
+ this.on("error", () => {
37569
+ });
37570
+ this.engine = engine;
37571
+ this.defaultTimeoutMs = cfg.defaultTimeoutMs;
37572
+ this.defaultMaxRetries = cfg.defaultMaxRetries;
37573
+ this.signal = signal;
37574
+ if (signal !== void 0) {
37575
+ this.wireAbortSignal(signal);
37576
+ }
37577
+ }
37578
+ // -----------------------------------------------------------------------
37579
+ // Public API
37580
+ // -----------------------------------------------------------------------
37581
+ /**
37582
+ * Creates a new DicomSend instance.
37583
+ *
37584
+ * @param options - Configuration options
37585
+ * @returns A Result containing the instance or a validation error
37586
+ */
37587
+ static create(options) {
37588
+ const validation = DicomSendOptionsSchema.safeParse(options);
37589
+ if (!validation.success) {
37590
+ return err(createValidationError("DicomSend", validation.error));
37591
+ }
37592
+ const cfg = resolveConfig2(options);
37593
+ const senderRef = { current: void 0 };
37594
+ const engine = new SenderEngine({
37595
+ ...cfg,
37596
+ executor: createDcmsendExecutor(options),
37597
+ signal: options.signal,
37598
+ senderName: "DicomSend",
37599
+ emitters: {
37600
+ emitSendComplete: (data) => {
37601
+ senderRef.current.emit("SEND_COMPLETE", data);
37602
+ },
37603
+ emitSendFailed: (data) => {
37604
+ senderRef.current.emit("SEND_FAILED", data);
37605
+ },
37606
+ emitHealthChanged: (data) => {
37607
+ senderRef.current.emit("HEALTH_CHANGED", data);
37608
+ },
37609
+ emitBucketFlushed: (data) => {
37610
+ senderRef.current.emit("BUCKET_FLUSHED", data);
37611
+ }
37612
+ }
37613
+ });
37614
+ const sender = new _DicomSend(engine, cfg, options.signal);
37615
+ senderRef.current = sender;
37616
+ return ok(sender);
37617
+ }
37618
+ /**
37619
+ * Sends one or more DICOM files to the remote endpoint.
37620
+ *
37621
+ * In single/multiple mode, files are sent as one dcmsend call.
37622
+ * In bucket mode, files are accumulated into a bucket and flushed
37623
+ * when the bucket reaches maxBucketSize or the flush timer fires.
37624
+ *
37625
+ * @param files - One or more DICOM file paths
37626
+ * @param options - Per-send overrides
37627
+ * @returns A Result containing the send result or an error
37628
+ */
37629
+ send(files, options) {
37630
+ const params = {
37631
+ calledAETitle: options?.calledAETitle,
37632
+ callingAETitle: options?.callingAETitle
37633
+ };
37634
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
37635
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
37636
+ return this.engine.send(files, timeoutMs, maxRetries, params);
37637
+ }
37638
+ /**
37639
+ * Flushes the current bucket immediately (bucket mode only).
37640
+ * In single/multiple mode this is a no-op.
37641
+ */
37642
+ flush() {
37643
+ this.engine.flush();
37644
+ }
37645
+ /**
37646
+ * Gracefully stops the sender. Rejects all queued items and
37647
+ * waits for active associations to complete.
37648
+ */
37649
+ async stop() {
37650
+ if (this.signal !== void 0 && this.abortHandler !== void 0) {
37651
+ this.signal.removeEventListener("abort", this.abortHandler);
37652
+ }
37653
+ await this.engine.stop();
37654
+ }
37655
+ /** Current sender status. */
37656
+ get status() {
37657
+ return this.engine.status;
37658
+ }
37659
+ // -----------------------------------------------------------------------
37660
+ // Typed event listener convenience methods
37661
+ // -----------------------------------------------------------------------
37662
+ /**
37663
+ * Registers a typed listener for a DicomSend-specific event.
37664
+ *
37665
+ * @param event - The event name from DicomSendEventMap
37666
+ * @param listener - Callback receiving typed event data
37667
+ * @returns this for chaining
37668
+ */
37669
+ onEvent(event, listener) {
37670
+ return this.on(event, listener);
37671
+ }
37672
+ /**
37673
+ * Registers a listener for successful sends.
37674
+ *
37675
+ * @param listener - Callback receiving send complete data
37676
+ * @returns this for chaining
37677
+ */
37678
+ onSendComplete(listener) {
37679
+ return this.on("SEND_COMPLETE", listener);
37680
+ }
37681
+ /**
37682
+ * Registers a listener for failed sends.
37683
+ *
37684
+ * @param listener - Callback receiving send failed data
37685
+ * @returns this for chaining
37686
+ */
37687
+ onSendFailed(listener) {
37688
+ return this.on("SEND_FAILED", listener);
37689
+ }
37690
+ /**
37691
+ * Registers a listener for health state changes.
37692
+ *
37693
+ * @param listener - Callback receiving health change data
37694
+ * @returns this for chaining
37695
+ */
37696
+ onHealthChanged(listener) {
37697
+ return this.on("HEALTH_CHANGED", listener);
37698
+ }
37699
+ /**
37700
+ * Registers a listener for bucket flushes (bucket mode only).
37701
+ *
37702
+ * @param listener - Callback receiving bucket flush data
37703
+ * @returns this for chaining
37704
+ */
37705
+ onBucketFlushed(listener) {
37706
+ return this.on("BUCKET_FLUSHED", listener);
37707
+ }
37708
+ // -----------------------------------------------------------------------
37709
+ // Abort signal
37710
+ // -----------------------------------------------------------------------
37711
+ /** Wires an AbortSignal to stop the sender. */
37712
+ wireAbortSignal(signal) {
37713
+ if (signal.aborted) {
37714
+ void this.stop();
37715
+ return;
37716
+ }
37717
+ this.abortHandler = () => {
37718
+ void this.stop();
37719
+ };
37720
+ signal.addEventListener("abort", this.abortHandler, { once: true });
37721
+ }
37722
+ };
37723
+
37386
37724
  // src/pacs/types.ts
37387
37725
  var QueryLevel = {
37388
37726
  STUDY: "STUDY",
@@ -37932,7 +38270,7 @@ var DEFAULT_CONFIG = {
37932
38270
  signal: void 0,
37933
38271
  onRetry: void 0
37934
38272
  };
37935
- function resolveConfig2(opts) {
38273
+ function resolveConfig3(opts) {
37936
38274
  if (!opts) return DEFAULT_CONFIG;
37937
38275
  return {
37938
38276
  ...DEFAULT_CONFIG,
@@ -37977,7 +38315,7 @@ function shouldBreakAfterFailure(attempt, lastError, config) {
37977
38315
  return false;
37978
38316
  }
37979
38317
  async function retry(operation, options) {
37980
- const config = resolveConfig2(options);
38318
+ const config = resolveConfig3(options);
37981
38319
  let lastResult = err(new Error("No attempts executed"));
37982
38320
  for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
37983
38321
  lastResult = await operation();
@@ -37991,6 +38329,6 @@ async function retry(operation, options) {
37991
38329
  return lastResult;
37992
38330
  }
37993
38331
 
37994
- 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, DicomInstance, DicomReceiver, DicomSender, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, 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 };
38332
+ 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, DicomInstance, DicomReceiver, DicomSend, DicomSender, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, 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 };
37995
38333
  //# sourceMappingURL=index.js.map
37996
38334
  //# sourceMappingURL=index.js.map