@ubercode/dcmtk 0.7.3 → 0.8.1

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
@@ -31159,6 +31159,7 @@ var Dcm2xmlOptionsSchema = z.object({
31159
31159
  charset: z.enum(["utf8", "latin1", "ascii"]).optional(),
31160
31160
  writeBinaryData: z.boolean().optional(),
31161
31161
  encodeBinaryBase64: z.boolean().optional(),
31162
+ charsetAssume: z.string().min(1).optional(),
31162
31163
  verbosity: z.enum(["verbose", "debug"]).optional()
31163
31164
  }).strict().optional();
31164
31165
  function pushEncodingArgs(args, options) {
@@ -31182,6 +31183,9 @@ function buildArgs(inputPath, options) {
31182
31183
  if (options?.namespace === true) {
31183
31184
  args.push("+Xn");
31184
31185
  }
31186
+ if (options?.charsetAssume !== void 0) {
31187
+ args.push("+Ca", options.charsetAssume);
31188
+ }
31185
31189
  pushEncodingArgs(args, options);
31186
31190
  args.push(inputPath);
31187
31191
  return args;
@@ -31431,6 +31435,7 @@ var Dcm2jsonOptionsSchema = z.object({
31431
31435
  timeoutMs: z.number().int().positive().optional(),
31432
31436
  signal: z.instanceof(AbortSignal).optional(),
31433
31437
  directOnly: z.boolean().optional(),
31438
+ charsetAssume: z.string().min(1).optional(),
31434
31439
  verbosity: z.enum(["verbose", "debug"]).optional()
31435
31440
  }).strict().optional();
31436
31441
  var VERBOSITY_FLAGS2 = { verbose: "-v", debug: "-d" };
@@ -31440,12 +31445,19 @@ function buildVerbosityArgs(verbosity) {
31440
31445
  }
31441
31446
  return [];
31442
31447
  }
31443
- async function tryXmlPath(inputPath, timeoutMs, signal, verbosity) {
31448
+ function buildXmlOpts(options) {
31449
+ const result = {};
31450
+ if (options?.verbosity !== void 0) result["verbosity"] = options.verbosity;
31451
+ if (options?.charsetAssume !== void 0) result["charsetAssume"] = options.charsetAssume;
31452
+ return result;
31453
+ }
31454
+ async function tryXmlPath(inputPath, timeoutMs, signal, opts) {
31444
31455
  const xmlBinary = resolveBinary("dcm2xml");
31445
31456
  if (!xmlBinary.ok) {
31446
31457
  return err(xmlBinary.error);
31447
31458
  }
31448
- const xmlArgs = [...buildVerbosityArgs(verbosity), "-nat", inputPath];
31459
+ const charsetArgs = opts?.charsetAssume !== void 0 ? ["+Ca", opts.charsetAssume] : [];
31460
+ const xmlArgs = [...buildVerbosityArgs(opts?.verbosity), ...charsetArgs, "-nat", inputPath];
31449
31461
  const xmlResult = await execCommand(xmlBinary.value, xmlArgs, { timeoutMs, signal });
31450
31462
  if (!xmlResult.ok) {
31451
31463
  return err(xmlResult.error);
@@ -31491,7 +31503,7 @@ async function dcm2json(inputPath, options) {
31491
31503
  if (options?.directOnly === true) {
31492
31504
  return tryDirectPath(inputPath, timeoutMs, signal, verbosity);
31493
31505
  }
31494
- const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, verbosity);
31506
+ const xmlResult = await tryXmlPath(inputPath, timeoutMs, signal, buildXmlOpts(options));
31495
31507
  if (xmlResult.ok) {
31496
31508
  return xmlResult;
31497
31509
  }
@@ -33106,6 +33118,30 @@ async function echoscu(options) {
33106
33118
  return ok({ success: true, stderr: result.value.stderr });
33107
33119
  }
33108
33120
  var VERBOSITY_FLAGS19 = { verbose: "-v", debug: "-d" };
33121
+ var DECOMPRESS_FLAGS = {
33122
+ never: "--decompress-never",
33123
+ lossless: "--decompress-lossless",
33124
+ lossy: "--decompress-lossy"
33125
+ };
33126
+ function pushSendModeArgs(args, options) {
33127
+ if (options.noHalt === true) {
33128
+ args.push("--no-halt");
33129
+ }
33130
+ if (options.noIllegalProposal === true) {
33131
+ args.push("--no-illegal-proposal");
33132
+ }
33133
+ if (options.decompress !== void 0) {
33134
+ args.push(DECOMPRESS_FLAGS[options.decompress]);
33135
+ }
33136
+ if (options.multiAssociations === true) {
33137
+ args.push("+ma");
33138
+ } else if (options.multiAssociations === false) {
33139
+ args.push("-ma");
33140
+ }
33141
+ if (options.createReportFile !== void 0) {
33142
+ args.push("--create-report-file", options.createReportFile);
33143
+ }
33144
+ }
33109
33145
  var DcmsendOptionsSchema = z.object({
33110
33146
  timeoutMs: z.number().int().positive().optional(),
33111
33147
  signal: z.instanceof(AbortSignal).optional(),
@@ -33115,8 +33151,15 @@ var DcmsendOptionsSchema = z.object({
33115
33151
  callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
33116
33152
  calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
33117
33153
  scanDirectory: z.boolean().optional(),
33154
+ recurse: z.boolean().optional(),
33155
+ scanPattern: z.string().min(1).optional(),
33118
33156
  verbosity: z.enum(["verbose", "debug"]).optional(),
33119
33157
  noUidChecks: z.boolean().optional(),
33158
+ noHalt: z.boolean().optional(),
33159
+ noIllegalProposal: z.boolean().optional(),
33160
+ decompress: z.enum(["never", "lossless", "lossy"]).optional(),
33161
+ multiAssociations: z.boolean().optional(),
33162
+ createReportFile: z.string().min(1).optional(),
33120
33163
  maxPduReceive: z.number().int().min(4096).max(131072).optional(),
33121
33164
  maxPduSend: z.number().int().min(4096).max(131072).optional(),
33122
33165
  noHostnameLookup: z.boolean().optional(),
@@ -33162,6 +33205,13 @@ function buildArgs33(options) {
33162
33205
  if (options.scanDirectory === true) {
33163
33206
  args.push("--scan-directories");
33164
33207
  }
33208
+ if (options.recurse === true) {
33209
+ args.push("+r");
33210
+ }
33211
+ if (options.scanPattern !== void 0) {
33212
+ args.push("--scan-pattern", options.scanPattern);
33213
+ }
33214
+ pushSendModeArgs(args, options);
33165
33215
  args.push(options.host, String(options.port));
33166
33216
  args.push(...options.files);
33167
33217
  return args;
@@ -34288,7 +34338,7 @@ var DicomInstance = class _DicomInstance {
34288
34338
  * Opens a DICOM file and creates a DicomInstance.
34289
34339
  *
34290
34340
  * @param path - Filesystem path to the DICOM file
34291
- * @param options - Timeout and abort options
34341
+ * @param options - Timeout, abort, and charset options
34292
34342
  * @returns A Result containing the DicomInstance or an error
34293
34343
  */
34294
34344
  static async open(path2, options) {
@@ -34296,7 +34346,8 @@ var DicomInstance = class _DicomInstance {
34296
34346
  if (!filePathResult.ok) return err(filePathResult.error);
34297
34347
  const jsonResult = await dcm2json(path2, {
34298
34348
  timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
34299
- signal: options?.signal
34349
+ signal: options?.signal,
34350
+ charsetAssume: options?.charsetAssume
34300
34351
  });
34301
34352
  if (!jsonResult.ok) return err(jsonResult.error);
34302
34353
  const datasetResult = DicomDataset.fromJson(jsonResult.value.data);
@@ -36852,68 +36903,13 @@ var SenderHealth = {
36852
36903
  DOWN: "down"
36853
36904
  };
36854
36905
 
36855
- // src/senders/DicomSender.ts
36856
- var DEFAULT_MAX_ASSOCIATIONS = 4;
36857
- var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
36858
- var DEFAULT_MAX_RETRIES = 3;
36859
- var DEFAULT_RETRY_DELAY_MS = 1e3;
36860
- var DEFAULT_BUCKET_FLUSH_MS = 5e3;
36861
- var DEFAULT_MAX_BUCKET_SIZE = 50;
36862
- var MAX_ASSOCIATIONS_LIMIT = 64;
36906
+ // src/senders/SenderEngine.ts
36863
36907
  var DEGRADE_THRESHOLD = 3;
36864
36908
  var DOWN_THRESHOLD = 10;
36865
36909
  var RECOVERY_THRESHOLD = 3;
36866
- var DicomSenderOptionsSchema = z.object({
36867
- host: z.string().min(1),
36868
- port: z.number().int().min(1).max(65535),
36869
- calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
36870
- callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
36871
- mode: z.enum(["single", "multiple", "bucket"]).optional(),
36872
- maxAssociations: z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
36873
- proposedTransferSyntax: z.union([z.enum(PROPOSED_TS_VALUES), z.array(z.enum(PROPOSED_TS_VALUES)).min(1)]).optional(),
36874
- combineProposedTransferSyntaxes: z.boolean().optional(),
36875
- maxQueueLength: z.number().int().min(1).optional(),
36876
- timeoutMs: z.number().int().positive().optional(),
36877
- maxRetries: z.number().int().min(0).optional(),
36878
- retryDelayMs: z.number().int().min(0).optional(),
36879
- bucketFlushMs: z.number().int().positive().optional(),
36880
- maxBucketSize: z.number().int().min(1).optional(),
36881
- maxPduReceive: z.number().int().min(4096).max(131072).optional(),
36882
- maxPduSend: z.number().int().min(4096).max(131072).optional(),
36883
- associationTimeout: z.number().int().positive().optional(),
36884
- acseTimeout: z.number().int().positive().optional(),
36885
- dimseTimeout: z.number().int().positive().optional(),
36886
- noHostnameLookup: z.boolean().optional(),
36887
- verbosity: z.enum(["verbose", "debug"]).optional(),
36888
- required: z.boolean().optional(),
36889
- signal: z.instanceof(AbortSignal).optional()
36890
- }).strict();
36891
- function resolveConfig(options) {
36892
- const mode = options.mode ?? "multiple";
36893
- const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
36894
- return {
36895
- mode,
36896
- configuredMaxAssociations: mode === "single" ? 1 : rawMax,
36897
- maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
36898
- defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
36899
- defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
36900
- retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
36901
- bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
36902
- maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
36903
- };
36904
- }
36905
- var DicomSender = class _DicomSender extends EventEmitter {
36906
- constructor(options) {
36907
- super();
36908
- __publicField(this, "options");
36909
- __publicField(this, "mode");
36910
- __publicField(this, "configuredMaxAssociations");
36911
- __publicField(this, "maxQueueLength");
36912
- __publicField(this, "defaultTimeoutMs");
36913
- __publicField(this, "defaultMaxRetries");
36914
- __publicField(this, "retryDelayMs");
36915
- __publicField(this, "bucketFlushMs");
36916
- __publicField(this, "maxBucketSize");
36910
+ var SenderEngine = class {
36911
+ constructor(config) {
36912
+ __publicField(this, "config");
36917
36913
  // Queue and concurrency state
36918
36914
  __publicField(this, "queue", []);
36919
36915
  __publicField(this, "activeAssociations", 0);
@@ -36926,113 +36922,36 @@ var DicomSender = class _DicomSender extends EventEmitter {
36926
36922
  // Bucket state (bucket mode only)
36927
36923
  __publicField(this, "currentBucket", []);
36928
36924
  __publicField(this, "bucketTimer");
36929
- // AbortSignal
36930
- __publicField(this, "abortHandler");
36931
- this.setMaxListeners(20);
36932
- this.on("error", () => {
36933
- });
36934
- this.options = options;
36935
- const cfg = resolveConfig(options);
36936
- this.mode = cfg.mode;
36937
- this.configuredMaxAssociations = cfg.configuredMaxAssociations;
36938
- this.effectiveMaxAssociations = cfg.configuredMaxAssociations;
36939
- this.maxQueueLength = cfg.maxQueueLength;
36940
- this.defaultTimeoutMs = cfg.defaultTimeoutMs;
36941
- this.defaultMaxRetries = cfg.defaultMaxRetries;
36942
- this.retryDelayMs = cfg.retryDelayMs;
36943
- this.bucketFlushMs = cfg.bucketFlushMs;
36944
- this.maxBucketSize = cfg.maxBucketSize;
36945
- if (options.signal !== void 0) {
36946
- this.wireAbortSignal(options.signal);
36947
- }
36925
+ this.config = config;
36926
+ this.effectiveMaxAssociations = config.configuredMaxAssociations;
36948
36927
  }
36949
36928
  // -----------------------------------------------------------------------
36950
36929
  // Public API
36951
36930
  // -----------------------------------------------------------------------
36952
- /**
36953
- * Creates a new DicomSender instance.
36954
- *
36955
- * @param options - Configuration options
36956
- * @returns A Result containing the instance or a validation error
36957
- */
36958
- static create(options) {
36959
- const validation = DicomSenderOptionsSchema.safeParse(options);
36960
- if (!validation.success) {
36961
- return err(createValidationError("DicomSender", validation.error));
36962
- }
36963
- return ok(new _DicomSender(options));
36964
- }
36965
- /**
36966
- * Sends one or more DICOM files to the remote endpoint.
36967
- *
36968
- * In single/multiple mode, files are sent as one storescu call.
36969
- * In bucket mode, files are accumulated into a bucket and flushed
36970
- * when the bucket reaches maxBucketSize or the flush timer fires.
36971
- *
36972
- * The returned promise resolves when the files are actually sent
36973
- * (not just queued). Callers can await for confirmation or
36974
- * fire-and-forget with `void sender.send(files)`.
36975
- *
36976
- * @param files - One or more DICOM file paths
36977
- * @param options - Per-send overrides
36978
- * @returns A Result containing the send result or an error
36979
- */
36980
- send(files, options) {
36931
+ /** Sends files through the engine's queue/bucket system. */
36932
+ send(files, timeoutMs, maxRetries, binaryParams) {
36981
36933
  if (this.isStopped) {
36982
- return Promise.resolve(err(new Error("DicomSender: sender is stopped")));
36934
+ return Promise.resolve(err(new Error(`${this.config.senderName}: sender is stopped`)));
36983
36935
  }
36984
36936
  if (files.length === 0) {
36985
- return Promise.resolve(err(new Error("DicomSender: no files provided")));
36986
- }
36987
- return this.dispatchSend(files, this.buildSendParams(options));
36988
- }
36989
- /** Builds SendParams from per-send options, applying instance defaults. */
36990
- buildSendParams(options) {
36991
- return {
36992
- timeoutMs: options?.timeoutMs ?? this.defaultTimeoutMs,
36993
- maxRetries: options?.maxRetries ?? this.defaultMaxRetries,
36994
- calledAETitle: options?.calledAETitle,
36995
- callingAETitle: options?.callingAETitle,
36996
- required: options?.required,
36997
- proposedTransferSyntax: options?.proposedTransferSyntax,
36998
- combineProposedTransferSyntaxes: options?.combineProposedTransferSyntaxes
36999
- };
37000
- }
37001
- /** Dispatches a send to the appropriate mode handler. */
37002
- dispatchSend(files, params) {
37003
- switch (this.mode) {
37004
- case "single":
37005
- case "multiple":
37006
- return this.enqueueSend(files, params);
37007
- case "bucket":
37008
- return this.enqueueBucket(files, params);
37009
- default:
37010
- assertUnreachable(this.mode);
36937
+ return Promise.resolve(err(new Error(`${this.config.senderName}: no files provided`)));
37011
36938
  }
36939
+ return this.dispatchSend(files, timeoutMs, maxRetries, binaryParams);
37012
36940
  }
37013
- /**
37014
- * Flushes the current bucket immediately (bucket mode only).
37015
- * In single/multiple mode this is a no-op.
37016
- */
36941
+ /** Flushes the current bucket immediately (bucket mode only). */
37017
36942
  flush() {
37018
- if (this.mode !== "bucket") return;
36943
+ if (this.config.mode !== "bucket") return;
37019
36944
  if (this.currentBucket.length === 0) return;
37020
36945
  this.clearBucketTimer();
37021
36946
  this.flushBucketInternal("timer");
37022
36947
  }
37023
- /**
37024
- * Gracefully stops the sender. Rejects all queued items and
37025
- * waits for active associations to complete.
37026
- */
36948
+ /** Gracefully stops the engine. Rejects all queued items and waits for active associations. */
37027
36949
  async stop() {
37028
36950
  if (this.isStopped) return;
37029
36951
  this.isStopped = true;
37030
- if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
37031
- this.options.signal.removeEventListener("abort", this.abortHandler);
37032
- }
37033
36952
  this.clearBucketTimer();
37034
- this.rejectBucket("DicomSender: sender stopped");
37035
- this.rejectQueue("DicomSender: sender stopped");
36953
+ this.rejectBucket(`${this.config.senderName}: sender stopped`);
36954
+ this.rejectQueue(`${this.config.senderName}: sender stopped`);
37036
36955
  await this.waitForActive();
37037
36956
  }
37038
36957
  /** Current sender status. */
@@ -37047,77 +36966,37 @@ var DicomSender = class _DicomSender extends EventEmitter {
37047
36966
  stopped: this.isStopped
37048
36967
  };
37049
36968
  }
36969
+ /** Whether the engine has been stopped. */
36970
+ get stopped() {
36971
+ return this.isStopped;
36972
+ }
37050
36973
  // -----------------------------------------------------------------------
37051
- // Typed event listener convenience methods
36974
+ // Dispatch
37052
36975
  // -----------------------------------------------------------------------
37053
- /**
37054
- * Registers a typed listener for a DicomSender-specific event.
37055
- *
37056
- * @param event - The event name from DicomSenderEventMap
37057
- * @param listener - Callback receiving typed event data
37058
- * @returns this for chaining
37059
- */
37060
- onEvent(event, listener) {
37061
- return this.on(event, listener);
37062
- }
37063
- /**
37064
- * Registers a listener for successful sends.
37065
- *
37066
- * @param listener - Callback receiving send complete data
37067
- * @returns this for chaining
37068
- */
37069
- onSendComplete(listener) {
37070
- return this.on("SEND_COMPLETE", listener);
37071
- }
37072
- /**
37073
- * Registers a listener for failed sends.
37074
- *
37075
- * @param listener - Callback receiving send failed data
37076
- * @returns this for chaining
37077
- */
37078
- onSendFailed(listener) {
37079
- return this.on("SEND_FAILED", listener);
37080
- }
37081
- /**
37082
- * Registers a listener for health state changes.
37083
- *
37084
- * @param listener - Callback receiving health change data
37085
- * @returns this for chaining
37086
- */
37087
- onHealthChanged(listener) {
37088
- return this.on("HEALTH_CHANGED", listener);
37089
- }
37090
- /**
37091
- * Registers a listener for bucket flushes (bucket mode only).
37092
- *
37093
- * @param listener - Callback receiving bucket flush data
37094
- * @returns this for chaining
37095
- */
37096
- onBucketFlushed(listener) {
37097
- return this.on("BUCKET_FLUSHED", listener);
36976
+ /** Dispatches a send to the appropriate mode handler. */
36977
+ dispatchSend(files, timeoutMs, maxRetries, binaryParams) {
36978
+ switch (this.config.mode) {
36979
+ case "single":
36980
+ case "multiple":
36981
+ return this.enqueueSend(files, timeoutMs, maxRetries, binaryParams);
36982
+ case "bucket":
36983
+ return this.enqueueBucket(files, timeoutMs, maxRetries, binaryParams);
36984
+ default:
36985
+ assertUnreachable(this.config.mode);
36986
+ }
37098
36987
  }
37099
36988
  // -----------------------------------------------------------------------
37100
36989
  // Single/Multiple mode: queue-based dispatch
37101
36990
  // -----------------------------------------------------------------------
37102
36991
  /** Enqueues a send and dispatches immediately if capacity allows. */
37103
- enqueueSend(files, params) {
36992
+ enqueueSend(files, timeoutMs, maxRetries, binaryParams) {
37104
36993
  return new Promise((resolve) => {
37105
36994
  const totalQueued = this.queue.length + this.currentBucket.length;
37106
- if (totalQueued >= this.maxQueueLength) {
37107
- resolve(err(new Error("DicomSender: queue full")));
36995
+ if (totalQueued >= this.config.maxQueueLength) {
36996
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37108
36997
  return;
37109
36998
  }
37110
- const entry = {
37111
- files,
37112
- timeoutMs: params.timeoutMs,
37113
- maxRetries: params.maxRetries,
37114
- calledAETitle: params.calledAETitle,
37115
- callingAETitle: params.callingAETitle,
37116
- required: params.required,
37117
- proposedTransferSyntax: params.proposedTransferSyntax,
37118
- combineProposedTransferSyntaxes: params.combineProposedTransferSyntaxes,
37119
- resolve
37120
- };
36999
+ const entry = { files, timeoutMs, maxRetries, binaryParams, resolve };
37121
37000
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37122
37001
  void this.executeEntry(entry);
37123
37002
  } else {
@@ -37137,27 +37016,16 @@ var DicomSender = class _DicomSender extends EventEmitter {
37137
37016
  // Bucket mode: accumulate-then-flush
37138
37017
  // -----------------------------------------------------------------------
37139
37018
  /** Adds files to the current bucket and triggers flush if full. */
37140
- enqueueBucket(files, params) {
37019
+ enqueueBucket(files, timeoutMs, maxRetries, binaryParams) {
37141
37020
  return new Promise((resolve) => {
37142
37021
  const totalQueued = this.queue.length + this.currentBucket.length;
37143
- if (totalQueued >= this.maxQueueLength) {
37144
- resolve(err(new Error("DicomSender: queue full")));
37022
+ if (totalQueued >= this.config.maxQueueLength) {
37023
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37145
37024
  return;
37146
37025
  }
37147
- const { timeoutMs, maxRetries, calledAETitle, callingAETitle, required, proposedTransferSyntax, combineProposedTransferSyntaxes } = params;
37148
- this.currentBucket.push({
37149
- files,
37150
- resolve,
37151
- timeoutMs,
37152
- maxRetries,
37153
- calledAETitle,
37154
- callingAETitle,
37155
- required,
37156
- proposedTransferSyntax,
37157
- combineProposedTransferSyntaxes
37158
- });
37026
+ this.currentBucket.push({ files, resolve, timeoutMs, maxRetries, binaryParams });
37159
37027
  const totalFiles = this.countBucketFiles();
37160
- if (totalFiles >= this.maxBucketSize) {
37028
+ if (totalFiles >= this.config.maxBucketSize) {
37161
37029
  this.clearBucketTimer();
37162
37030
  void this.flushBucketInternal("maxSize");
37163
37031
  } else {
@@ -37189,11 +37057,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37189
37057
  files: allFiles,
37190
37058
  timeoutMs,
37191
37059
  maxRetries,
37192
- calledAETitle: entries[0]?.calledAETitle,
37193
- callingAETitle: entries[0]?.callingAETitle,
37194
- required: entries[0]?.required,
37195
- proposedTransferSyntax: entries[0]?.proposedTransferSyntax,
37196
- combineProposedTransferSyntaxes: entries[0]?.combineProposedTransferSyntaxes,
37060
+ binaryParams: entries[0].binaryParams,
37197
37061
  resolve: (result) => {
37198
37062
  for (let i = 0; i < entries.length; i++) {
37199
37063
  entries[i].resolve(result);
@@ -37207,7 +37071,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37207
37071
  const entries = [...this.currentBucket];
37208
37072
  this.currentBucket = [];
37209
37073
  const bucketEntry = this.mergeBucketEntries(entries);
37210
- this.emit("BUCKET_FLUSHED", { fileCount: bucketEntry.files.length, reason });
37074
+ this.config.emitters.emitBucketFlushed({ fileCount: bucketEntry.files.length, reason });
37211
37075
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37212
37076
  void this.executeEntry(bucketEntry);
37213
37077
  } else {
@@ -37220,7 +37084,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37220
37084
  this.bucketTimer = setTimeout(() => {
37221
37085
  this.bucketTimer = void 0;
37222
37086
  void this.flushBucketInternal("timer");
37223
- }, this.bucketFlushMs);
37087
+ }, this.config.bucketFlushMs);
37224
37088
  }
37225
37089
  /** Clears the bucket flush timer. */
37226
37090
  clearBucketTimer() {
@@ -37232,7 +37096,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37232
37096
  // -----------------------------------------------------------------------
37233
37097
  // Core send execution with retry
37234
37098
  // -----------------------------------------------------------------------
37235
- /** Executes a single queue entry: calls storescu with retry. */
37099
+ /** Executes a single queue entry: calls the binary with retry. */
37236
37100
  async executeEntry(entry) {
37237
37101
  this.activeAssociations++;
37238
37102
  const startMs = Date.now();
@@ -37241,7 +37105,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37241
37105
  if (failure === void 0) return;
37242
37106
  this.activeAssociations--;
37243
37107
  this.recordFailure();
37244
- this.emit("SEND_FAILED", {
37108
+ this.config.emitters.emitSendFailed({
37245
37109
  files: entry.files,
37246
37110
  error: failure.error,
37247
37111
  attempts: maxAttempts,
@@ -37251,49 +37115,27 @@ var DicomSender = class _DicomSender extends EventEmitter {
37251
37115
  entry.resolve(err(failure.error));
37252
37116
  this.drainQueue();
37253
37117
  }
37254
- /** Attempts storescu up to maxAttempts times. Returns undefined on success, or failure info. */
37118
+ /** Attempts the binary call up to maxAttempts times. Returns undefined on success. */
37255
37119
  async attemptSend(entry, maxAttempts, startMs) {
37256
37120
  let lastError;
37257
37121
  const lastOutput = { stdout: "", stderr: "" };
37258
37122
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
37259
37123
  if (this.isStopped) {
37260
37124
  this.activeAssociations--;
37261
- entry.resolve(err(new Error("DicomSender: sender stopped")));
37125
+ entry.resolve(err(new Error(`${this.config.senderName}: sender stopped`)));
37262
37126
  return void 0;
37263
37127
  }
37264
- const result = await this.callStorescu(entry);
37128
+ const result = await this.config.executor(entry.files, entry.timeoutMs, entry.binaryParams, this.config.signal);
37265
37129
  if (result.ok) {
37266
37130
  this.handleSendSuccess(entry, startMs, result.value);
37267
37131
  return void 0;
37268
37132
  }
37269
37133
  lastError = result.error;
37270
37134
  if (attempt < maxAttempts - 1) {
37271
- await delay2(this.retryDelayMs * (attempt + 1));
37135
+ await delay2(this.config.retryDelayMs * (attempt + 1));
37272
37136
  }
37273
37137
  }
37274
- return { error: lastError ?? new Error("DicomSender: send failed"), output: lastOutput };
37275
- }
37276
- /** Calls storescu with the configured options. */
37277
- callStorescu(entry) {
37278
- return storescu({
37279
- host: this.options.host,
37280
- port: this.options.port,
37281
- files: [...entry.files],
37282
- calledAETitle: entry.calledAETitle ?? this.options.calledAETitle,
37283
- callingAETitle: entry.callingAETitle ?? this.options.callingAETitle,
37284
- proposedTransferSyntax: entry.proposedTransferSyntax ?? this.options.proposedTransferSyntax,
37285
- combineProposedTransferSyntaxes: entry.combineProposedTransferSyntaxes ?? this.options.combineProposedTransferSyntaxes,
37286
- maxPduReceive: this.options.maxPduReceive,
37287
- maxPduSend: this.options.maxPduSend,
37288
- associationTimeout: this.options.associationTimeout,
37289
- acseTimeout: this.options.acseTimeout,
37290
- dimseTimeout: this.options.dimseTimeout,
37291
- noHostnameLookup: this.options.noHostnameLookup,
37292
- verbosity: this.options.verbosity,
37293
- required: entry.required ?? this.options.required,
37294
- timeoutMs: entry.timeoutMs,
37295
- signal: this.options.signal
37296
- });
37138
+ return { error: lastError ?? new Error(`${this.config.senderName}: send failed`), output: lastOutput };
37297
37139
  }
37298
37140
  /** Handles a successful send: updates state, emits event, resolves promise. */
37299
37141
  handleSendSuccess(entry, startMs, output) {
@@ -37301,7 +37143,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37301
37143
  const durationMs = Date.now() - startMs;
37302
37144
  this.recordSuccess();
37303
37145
  const data = { files: entry.files, fileCount: entry.files.length, durationMs, stdout: output.stdout, stderr: output.stderr };
37304
- this.emit("SEND_COMPLETE", data);
37146
+ this.config.emitters.emitSendComplete(data);
37305
37147
  entry.resolve(ok(data));
37306
37148
  this.drainQueue();
37307
37149
  }
@@ -37319,8 +37161,8 @@ var DicomSender = class _DicomSender extends EventEmitter {
37319
37161
  if (this.health === SenderHealth.DOWN) {
37320
37162
  this.health = SenderHealth.DEGRADED;
37321
37163
  } else {
37322
- this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.configuredMaxAssociations);
37323
- if (this.effectiveMaxAssociations >= this.configuredMaxAssociations) {
37164
+ this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.config.configuredMaxAssociations);
37165
+ if (this.effectiveMaxAssociations >= this.config.configuredMaxAssociations) {
37324
37166
  this.health = SenderHealth.HEALTHY;
37325
37167
  }
37326
37168
  }
@@ -37340,22 +37182,26 @@ var DicomSender = class _DicomSender extends EventEmitter {
37340
37182
  this.emitHealthChanged(previousHealth);
37341
37183
  }
37342
37184
  } else if (this.consecutiveFailures >= DEGRADE_THRESHOLD && this.consecutiveFailures % DEGRADE_THRESHOLD === 0) {
37343
- if (this.health === SenderHealth.HEALTHY) {
37344
- this.health = SenderHealth.DEGRADED;
37345
- this.effectiveMaxAssociations = Math.max(1, Math.floor(this.configuredMaxAssociations / 2));
37185
+ this.degradeHealth(previousHealth);
37186
+ }
37187
+ }
37188
+ /** Handles HEALTHY→DEGRADED or DEGRADED→DEGRADED transitions. */
37189
+ degradeHealth(previousHealth) {
37190
+ if (this.health === SenderHealth.HEALTHY) {
37191
+ this.health = SenderHealth.DEGRADED;
37192
+ this.effectiveMaxAssociations = Math.max(1, Math.floor(this.config.configuredMaxAssociations / 2));
37193
+ this.emitHealthChanged(previousHealth);
37194
+ } else if (this.health === SenderHealth.DEGRADED) {
37195
+ const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
37196
+ if (newMax !== this.effectiveMaxAssociations) {
37197
+ this.effectiveMaxAssociations = newMax;
37346
37198
  this.emitHealthChanged(previousHealth);
37347
- } else if (this.health === SenderHealth.DEGRADED) {
37348
- const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
37349
- if (newMax !== this.effectiveMaxAssociations) {
37350
- this.effectiveMaxAssociations = newMax;
37351
- this.emitHealthChanged(previousHealth);
37352
- }
37353
37199
  }
37354
37200
  }
37355
37201
  }
37356
- /** Emits a HEALTH_CHANGED event. */
37202
+ /** Emits a HEALTH_CHANGED event via the wrapper's emitter. */
37357
37203
  emitHealthChanged(previousHealth) {
37358
- this.emit("HEALTH_CHANGED", {
37204
+ this.config.emitters.emitHealthChanged({
37359
37205
  previousHealth,
37360
37206
  newHealth: this.health,
37361
37207
  effectiveMaxAssociations: this.effectiveMaxAssociations,
@@ -37395,20 +37241,6 @@ var DicomSender = class _DicomSender extends EventEmitter {
37395
37241
  check();
37396
37242
  });
37397
37243
  }
37398
- // -----------------------------------------------------------------------
37399
- // Abort signal
37400
- // -----------------------------------------------------------------------
37401
- /** Wires an AbortSignal to stop the sender. */
37402
- wireAbortSignal(signal) {
37403
- if (signal.aborted) {
37404
- void this.stop();
37405
- return;
37406
- }
37407
- this.abortHandler = () => {
37408
- void this.stop();
37409
- };
37410
- signal.addEventListener("abort", this.abortHandler, { once: true });
37411
- }
37412
37244
  };
37413
37245
  function delay2(ms) {
37414
37246
  return new Promise((resolve) => {
@@ -37416,6 +37248,492 @@ function delay2(ms) {
37416
37248
  });
37417
37249
  }
37418
37250
 
37251
+ // src/senders/DicomSender.ts
37252
+ var DEFAULT_MAX_ASSOCIATIONS = 4;
37253
+ var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
37254
+ var DEFAULT_MAX_RETRIES = 3;
37255
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
37256
+ var DEFAULT_BUCKET_FLUSH_MS = 5e3;
37257
+ var DEFAULT_MAX_BUCKET_SIZE = 50;
37258
+ var MAX_ASSOCIATIONS_LIMIT = 64;
37259
+ var DicomSenderOptionsSchema = z.object({
37260
+ host: z.string().min(1),
37261
+ port: z.number().int().min(1).max(65535),
37262
+ calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37263
+ callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37264
+ mode: z.enum(["single", "multiple", "bucket"]).optional(),
37265
+ maxAssociations: z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
37266
+ proposedTransferSyntax: z.union([z.enum(PROPOSED_TS_VALUES), z.array(z.enum(PROPOSED_TS_VALUES)).min(1)]).optional(),
37267
+ combineProposedTransferSyntaxes: z.boolean().optional(),
37268
+ maxQueueLength: z.number().int().min(1).optional(),
37269
+ timeoutMs: z.number().int().positive().optional(),
37270
+ maxRetries: z.number().int().min(0).optional(),
37271
+ retryDelayMs: z.number().int().min(0).optional(),
37272
+ bucketFlushMs: z.number().int().positive().optional(),
37273
+ maxBucketSize: z.number().int().min(1).optional(),
37274
+ maxPduReceive: z.number().int().min(4096).max(131072).optional(),
37275
+ maxPduSend: z.number().int().min(4096).max(131072).optional(),
37276
+ associationTimeout: z.number().int().positive().optional(),
37277
+ acseTimeout: z.number().int().positive().optional(),
37278
+ dimseTimeout: z.number().int().positive().optional(),
37279
+ noHostnameLookup: z.boolean().optional(),
37280
+ verbosity: z.enum(["verbose", "debug"]).optional(),
37281
+ required: z.boolean().optional(),
37282
+ signal: z.instanceof(AbortSignal).optional()
37283
+ }).strict();
37284
+ function createStorescuExecutor(options) {
37285
+ return async (files, timeoutMs, params, signal) => {
37286
+ const result = await storescu({
37287
+ host: options.host,
37288
+ port: options.port,
37289
+ files: [...files],
37290
+ calledAETitle: params.calledAETitle ?? options.calledAETitle,
37291
+ callingAETitle: params.callingAETitle ?? options.callingAETitle,
37292
+ proposedTransferSyntax: params.proposedTransferSyntax ?? options.proposedTransferSyntax,
37293
+ combineProposedTransferSyntaxes: params.combineProposedTransferSyntaxes ?? options.combineProposedTransferSyntaxes,
37294
+ maxPduReceive: options.maxPduReceive,
37295
+ maxPduSend: options.maxPduSend,
37296
+ associationTimeout: options.associationTimeout,
37297
+ acseTimeout: options.acseTimeout,
37298
+ dimseTimeout: options.dimseTimeout,
37299
+ noHostnameLookup: options.noHostnameLookup,
37300
+ verbosity: options.verbosity,
37301
+ required: params.required ?? options.required,
37302
+ timeoutMs,
37303
+ signal
37304
+ });
37305
+ if (!result.ok) return err(result.error);
37306
+ return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37307
+ };
37308
+ }
37309
+ function resolveConfig(options) {
37310
+ const mode = options.mode ?? "multiple";
37311
+ const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
37312
+ return {
37313
+ mode,
37314
+ configuredMaxAssociations: mode === "single" ? 1 : rawMax,
37315
+ maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
37316
+ defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
37317
+ defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
37318
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
37319
+ bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
37320
+ maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
37321
+ };
37322
+ }
37323
+ var DicomSender = class _DicomSender extends EventEmitter {
37324
+ constructor(engine, cfg, signal) {
37325
+ super();
37326
+ __publicField(this, "engine");
37327
+ __publicField(this, "defaultTimeoutMs");
37328
+ __publicField(this, "defaultMaxRetries");
37329
+ __publicField(this, "signal");
37330
+ __publicField(this, "abortHandler");
37331
+ this.setMaxListeners(20);
37332
+ this.on("error", () => {
37333
+ });
37334
+ this.engine = engine;
37335
+ this.defaultTimeoutMs = cfg.defaultTimeoutMs;
37336
+ this.defaultMaxRetries = cfg.defaultMaxRetries;
37337
+ this.signal = signal;
37338
+ if (signal !== void 0) {
37339
+ this.wireAbortSignal(signal);
37340
+ }
37341
+ }
37342
+ // -----------------------------------------------------------------------
37343
+ // Public API
37344
+ // -----------------------------------------------------------------------
37345
+ /**
37346
+ * Creates a new DicomSender instance.
37347
+ *
37348
+ * @param options - Configuration options
37349
+ * @returns A Result containing the instance or a validation error
37350
+ */
37351
+ static create(options) {
37352
+ const validation = DicomSenderOptionsSchema.safeParse(options);
37353
+ if (!validation.success) {
37354
+ return err(createValidationError("DicomSender", validation.error));
37355
+ }
37356
+ const cfg = resolveConfig(options);
37357
+ const senderRef = { current: void 0 };
37358
+ const engine = new SenderEngine({
37359
+ ...cfg,
37360
+ executor: createStorescuExecutor(options),
37361
+ signal: options.signal,
37362
+ senderName: "DicomSender",
37363
+ emitters: {
37364
+ emitSendComplete: (data) => {
37365
+ senderRef.current.emit("SEND_COMPLETE", data);
37366
+ },
37367
+ emitSendFailed: (data) => {
37368
+ senderRef.current.emit("SEND_FAILED", data);
37369
+ },
37370
+ emitHealthChanged: (data) => {
37371
+ senderRef.current.emit("HEALTH_CHANGED", data);
37372
+ },
37373
+ emitBucketFlushed: (data) => {
37374
+ senderRef.current.emit("BUCKET_FLUSHED", data);
37375
+ }
37376
+ }
37377
+ });
37378
+ const sender = new _DicomSender(engine, cfg, options.signal);
37379
+ senderRef.current = sender;
37380
+ return ok(sender);
37381
+ }
37382
+ /**
37383
+ * Sends one or more DICOM files to the remote endpoint.
37384
+ *
37385
+ * In single/multiple mode, files are sent as one storescu call.
37386
+ * In bucket mode, files are accumulated into a bucket and flushed
37387
+ * when the bucket reaches maxBucketSize or the flush timer fires.
37388
+ *
37389
+ * The returned promise resolves when the files are actually sent
37390
+ * (not just queued). Callers can await for confirmation or
37391
+ * fire-and-forget with `void sender.send(files)`.
37392
+ *
37393
+ * @param files - One or more DICOM file paths
37394
+ * @param options - Per-send overrides
37395
+ * @returns A Result containing the send result or an error
37396
+ */
37397
+ send(files, options) {
37398
+ const params = buildStorescuParams(options);
37399
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
37400
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
37401
+ return this.engine.send(files, timeoutMs, maxRetries, params);
37402
+ }
37403
+ /**
37404
+ * Flushes the current bucket immediately (bucket mode only).
37405
+ * In single/multiple mode this is a no-op.
37406
+ */
37407
+ flush() {
37408
+ this.engine.flush();
37409
+ }
37410
+ /**
37411
+ * Gracefully stops the sender. Rejects all queued items and
37412
+ * waits for active associations to complete.
37413
+ */
37414
+ async stop() {
37415
+ if (this.signal !== void 0 && this.abortHandler !== void 0) {
37416
+ this.signal.removeEventListener("abort", this.abortHandler);
37417
+ }
37418
+ await this.engine.stop();
37419
+ }
37420
+ /** Current sender status. */
37421
+ get status() {
37422
+ return this.engine.status;
37423
+ }
37424
+ // -----------------------------------------------------------------------
37425
+ // Typed event listener convenience methods
37426
+ // -----------------------------------------------------------------------
37427
+ /**
37428
+ * Registers a typed listener for a DicomSender-specific event.
37429
+ *
37430
+ * @param event - The event name from DicomSenderEventMap
37431
+ * @param listener - Callback receiving typed event data
37432
+ * @returns this for chaining
37433
+ */
37434
+ onEvent(event, listener) {
37435
+ return this.on(event, listener);
37436
+ }
37437
+ /**
37438
+ * Registers a listener for successful sends.
37439
+ *
37440
+ * @param listener - Callback receiving send complete data
37441
+ * @returns this for chaining
37442
+ */
37443
+ onSendComplete(listener) {
37444
+ return this.on("SEND_COMPLETE", listener);
37445
+ }
37446
+ /**
37447
+ * Registers a listener for failed sends.
37448
+ *
37449
+ * @param listener - Callback receiving send failed data
37450
+ * @returns this for chaining
37451
+ */
37452
+ onSendFailed(listener) {
37453
+ return this.on("SEND_FAILED", listener);
37454
+ }
37455
+ /**
37456
+ * Registers a listener for health state changes.
37457
+ *
37458
+ * @param listener - Callback receiving health change data
37459
+ * @returns this for chaining
37460
+ */
37461
+ onHealthChanged(listener) {
37462
+ return this.on("HEALTH_CHANGED", listener);
37463
+ }
37464
+ /**
37465
+ * Registers a listener for bucket flushes (bucket mode only).
37466
+ *
37467
+ * @param listener - Callback receiving bucket flush data
37468
+ * @returns this for chaining
37469
+ */
37470
+ onBucketFlushed(listener) {
37471
+ return this.on("BUCKET_FLUSHED", listener);
37472
+ }
37473
+ // -----------------------------------------------------------------------
37474
+ // Abort signal
37475
+ // -----------------------------------------------------------------------
37476
+ /** Wires an AbortSignal to stop the sender. */
37477
+ wireAbortSignal(signal) {
37478
+ if (signal.aborted) {
37479
+ void this.stop();
37480
+ return;
37481
+ }
37482
+ this.abortHandler = () => {
37483
+ void this.stop();
37484
+ };
37485
+ signal.addEventListener("abort", this.abortHandler, { once: true });
37486
+ }
37487
+ };
37488
+ function buildStorescuParams(options) {
37489
+ return {
37490
+ calledAETitle: options?.calledAETitle,
37491
+ callingAETitle: options?.callingAETitle,
37492
+ required: options?.required,
37493
+ proposedTransferSyntax: options?.proposedTransferSyntax,
37494
+ combineProposedTransferSyntaxes: options?.combineProposedTransferSyntaxes
37495
+ };
37496
+ }
37497
+ var DEFAULT_MAX_ASSOCIATIONS2 = 4;
37498
+ var DEFAULT_MAX_QUEUE_LENGTH2 = 1e3;
37499
+ var DEFAULT_MAX_RETRIES2 = 3;
37500
+ var DEFAULT_RETRY_DELAY_MS2 = 1e3;
37501
+ var DEFAULT_BUCKET_FLUSH_MS2 = 5e3;
37502
+ var DEFAULT_MAX_BUCKET_SIZE2 = 50;
37503
+ var MAX_ASSOCIATIONS_LIMIT2 = 64;
37504
+ var DicomSendOptionsSchema = z.object({
37505
+ host: z.string().min(1),
37506
+ port: z.number().int().min(1).max(65535),
37507
+ calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37508
+ callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37509
+ mode: z.enum(["single", "multiple", "bucket"]).optional(),
37510
+ maxAssociations: z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT2).optional(),
37511
+ maxQueueLength: z.number().int().min(1).optional(),
37512
+ timeoutMs: z.number().int().positive().optional(),
37513
+ maxRetries: z.number().int().min(0).optional(),
37514
+ retryDelayMs: z.number().int().min(0).optional(),
37515
+ bucketFlushMs: z.number().int().positive().optional(),
37516
+ maxBucketSize: z.number().int().min(1).optional(),
37517
+ maxPduReceive: z.number().int().min(4096).max(131072).optional(),
37518
+ maxPduSend: z.number().int().min(4096).max(131072).optional(),
37519
+ associationTimeout: z.number().int().positive().optional(),
37520
+ acseTimeout: z.number().int().positive().optional(),
37521
+ dimseTimeout: z.number().int().positive().optional(),
37522
+ noHostnameLookup: z.boolean().optional(),
37523
+ verbosity: z.enum(["verbose", "debug"]).optional(),
37524
+ noHalt: z.boolean().optional(),
37525
+ noIllegalProposal: z.boolean().optional(),
37526
+ decompress: z.enum(["never", "lossless", "lossy"]).optional(),
37527
+ multiAssociations: z.boolean().optional(),
37528
+ noUidChecks: z.boolean().optional(),
37529
+ signal: z.instanceof(AbortSignal).optional()
37530
+ }).strict();
37531
+ function createDcmsendExecutor(options) {
37532
+ return async (files, timeoutMs, params, signal) => {
37533
+ const result = await dcmsend({
37534
+ host: options.host,
37535
+ port: options.port,
37536
+ files: [...files],
37537
+ calledAETitle: params.calledAETitle ?? options.calledAETitle,
37538
+ callingAETitle: params.callingAETitle ?? options.callingAETitle,
37539
+ maxPduReceive: options.maxPduReceive,
37540
+ maxPduSend: options.maxPduSend,
37541
+ associationTimeout: options.associationTimeout,
37542
+ acseTimeout: options.acseTimeout,
37543
+ dimseTimeout: options.dimseTimeout,
37544
+ noHostnameLookup: options.noHostnameLookup,
37545
+ verbosity: options.verbosity,
37546
+ noHalt: options.noHalt,
37547
+ noIllegalProposal: options.noIllegalProposal,
37548
+ decompress: options.decompress,
37549
+ multiAssociations: options.multiAssociations,
37550
+ noUidChecks: options.noUidChecks,
37551
+ timeoutMs,
37552
+ signal
37553
+ });
37554
+ if (!result.ok) return err(result.error);
37555
+ return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37556
+ };
37557
+ }
37558
+ function resolveConfig2(options) {
37559
+ const mode = options.mode ?? "multiple";
37560
+ const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS2;
37561
+ return {
37562
+ mode,
37563
+ configuredMaxAssociations: mode === "single" ? 1 : rawMax,
37564
+ maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH2,
37565
+ defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
37566
+ defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES2,
37567
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
37568
+ bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS2,
37569
+ maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE2
37570
+ };
37571
+ }
37572
+ var DicomSend = class _DicomSend extends EventEmitter {
37573
+ constructor(engine, cfg, signal) {
37574
+ super();
37575
+ __publicField(this, "engine");
37576
+ __publicField(this, "defaultTimeoutMs");
37577
+ __publicField(this, "defaultMaxRetries");
37578
+ __publicField(this, "signal");
37579
+ __publicField(this, "abortHandler");
37580
+ this.setMaxListeners(20);
37581
+ this.on("error", () => {
37582
+ });
37583
+ this.engine = engine;
37584
+ this.defaultTimeoutMs = cfg.defaultTimeoutMs;
37585
+ this.defaultMaxRetries = cfg.defaultMaxRetries;
37586
+ this.signal = signal;
37587
+ if (signal !== void 0) {
37588
+ this.wireAbortSignal(signal);
37589
+ }
37590
+ }
37591
+ // -----------------------------------------------------------------------
37592
+ // Public API
37593
+ // -----------------------------------------------------------------------
37594
+ /**
37595
+ * Creates a new DicomSend instance.
37596
+ *
37597
+ * @param options - Configuration options
37598
+ * @returns A Result containing the instance or a validation error
37599
+ */
37600
+ static create(options) {
37601
+ const validation = DicomSendOptionsSchema.safeParse(options);
37602
+ if (!validation.success) {
37603
+ return err(createValidationError("DicomSend", validation.error));
37604
+ }
37605
+ const cfg = resolveConfig2(options);
37606
+ const senderRef = { current: void 0 };
37607
+ const engine = new SenderEngine({
37608
+ ...cfg,
37609
+ executor: createDcmsendExecutor(options),
37610
+ signal: options.signal,
37611
+ senderName: "DicomSend",
37612
+ emitters: {
37613
+ emitSendComplete: (data) => {
37614
+ senderRef.current.emit("SEND_COMPLETE", data);
37615
+ },
37616
+ emitSendFailed: (data) => {
37617
+ senderRef.current.emit("SEND_FAILED", data);
37618
+ },
37619
+ emitHealthChanged: (data) => {
37620
+ senderRef.current.emit("HEALTH_CHANGED", data);
37621
+ },
37622
+ emitBucketFlushed: (data) => {
37623
+ senderRef.current.emit("BUCKET_FLUSHED", data);
37624
+ }
37625
+ }
37626
+ });
37627
+ const sender = new _DicomSend(engine, cfg, options.signal);
37628
+ senderRef.current = sender;
37629
+ return ok(sender);
37630
+ }
37631
+ /**
37632
+ * Sends one or more DICOM files to the remote endpoint.
37633
+ *
37634
+ * In single/multiple mode, files are sent as one dcmsend call.
37635
+ * In bucket mode, files are accumulated into a bucket and flushed
37636
+ * when the bucket reaches maxBucketSize or the flush timer fires.
37637
+ *
37638
+ * @param files - One or more DICOM file paths
37639
+ * @param options - Per-send overrides
37640
+ * @returns A Result containing the send result or an error
37641
+ */
37642
+ send(files, options) {
37643
+ const params = {
37644
+ calledAETitle: options?.calledAETitle,
37645
+ callingAETitle: options?.callingAETitle
37646
+ };
37647
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
37648
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
37649
+ return this.engine.send(files, timeoutMs, maxRetries, params);
37650
+ }
37651
+ /**
37652
+ * Flushes the current bucket immediately (bucket mode only).
37653
+ * In single/multiple mode this is a no-op.
37654
+ */
37655
+ flush() {
37656
+ this.engine.flush();
37657
+ }
37658
+ /**
37659
+ * Gracefully stops the sender. Rejects all queued items and
37660
+ * waits for active associations to complete.
37661
+ */
37662
+ async stop() {
37663
+ if (this.signal !== void 0 && this.abortHandler !== void 0) {
37664
+ this.signal.removeEventListener("abort", this.abortHandler);
37665
+ }
37666
+ await this.engine.stop();
37667
+ }
37668
+ /** Current sender status. */
37669
+ get status() {
37670
+ return this.engine.status;
37671
+ }
37672
+ // -----------------------------------------------------------------------
37673
+ // Typed event listener convenience methods
37674
+ // -----------------------------------------------------------------------
37675
+ /**
37676
+ * Registers a typed listener for a DicomSend-specific event.
37677
+ *
37678
+ * @param event - The event name from DicomSendEventMap
37679
+ * @param listener - Callback receiving typed event data
37680
+ * @returns this for chaining
37681
+ */
37682
+ onEvent(event, listener) {
37683
+ return this.on(event, listener);
37684
+ }
37685
+ /**
37686
+ * Registers a listener for successful sends.
37687
+ *
37688
+ * @param listener - Callback receiving send complete data
37689
+ * @returns this for chaining
37690
+ */
37691
+ onSendComplete(listener) {
37692
+ return this.on("SEND_COMPLETE", listener);
37693
+ }
37694
+ /**
37695
+ * Registers a listener for failed sends.
37696
+ *
37697
+ * @param listener - Callback receiving send failed data
37698
+ * @returns this for chaining
37699
+ */
37700
+ onSendFailed(listener) {
37701
+ return this.on("SEND_FAILED", listener);
37702
+ }
37703
+ /**
37704
+ * Registers a listener for health state changes.
37705
+ *
37706
+ * @param listener - Callback receiving health change data
37707
+ * @returns this for chaining
37708
+ */
37709
+ onHealthChanged(listener) {
37710
+ return this.on("HEALTH_CHANGED", listener);
37711
+ }
37712
+ /**
37713
+ * Registers a listener for bucket flushes (bucket mode only).
37714
+ *
37715
+ * @param listener - Callback receiving bucket flush data
37716
+ * @returns this for chaining
37717
+ */
37718
+ onBucketFlushed(listener) {
37719
+ return this.on("BUCKET_FLUSHED", listener);
37720
+ }
37721
+ // -----------------------------------------------------------------------
37722
+ // Abort signal
37723
+ // -----------------------------------------------------------------------
37724
+ /** Wires an AbortSignal to stop the sender. */
37725
+ wireAbortSignal(signal) {
37726
+ if (signal.aborted) {
37727
+ void this.stop();
37728
+ return;
37729
+ }
37730
+ this.abortHandler = () => {
37731
+ void this.stop();
37732
+ };
37733
+ signal.addEventListener("abort", this.abortHandler, { once: true });
37734
+ }
37735
+ };
37736
+
37419
37737
  // src/pacs/types.ts
37420
37738
  var QueryLevel = {
37421
37739
  STUDY: "STUDY",
@@ -37965,7 +38283,7 @@ var DEFAULT_CONFIG = {
37965
38283
  signal: void 0,
37966
38284
  onRetry: void 0
37967
38285
  };
37968
- function resolveConfig2(opts) {
38286
+ function resolveConfig3(opts) {
37969
38287
  if (!opts) return DEFAULT_CONFIG;
37970
38288
  return {
37971
38289
  ...DEFAULT_CONFIG,
@@ -38010,7 +38328,7 @@ function shouldBreakAfterFailure(attempt, lastError, config) {
38010
38328
  return false;
38011
38329
  }
38012
38330
  async function retry(operation, options) {
38013
- const config = resolveConfig2(options);
38331
+ const config = resolveConfig3(options);
38014
38332
  let lastResult = err(new Error("No attempts executed"));
38015
38333
  for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
38016
38334
  lastResult = await operation();
@@ -38024,6 +38342,6 @@ async function retry(operation, options) {
38024
38342
  return lastResult;
38025
38343
  }
38026
38344
 
38027
- 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 };
38345
+ 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 };
38028
38346
  //# sourceMappingURL=index.js.map
38029
38347
  //# sourceMappingURL=index.js.map