@ubercode/dcmtk 0.7.3 → 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;
@@ -36852,68 +36890,13 @@ var SenderHealth = {
36852
36890
  DOWN: "down"
36853
36891
  };
36854
36892
 
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;
36893
+ // src/senders/SenderEngine.ts
36863
36894
  var DEGRADE_THRESHOLD = 3;
36864
36895
  var DOWN_THRESHOLD = 10;
36865
36896
  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");
36897
+ var SenderEngine = class {
36898
+ constructor(config) {
36899
+ __publicField(this, "config");
36917
36900
  // Queue and concurrency state
36918
36901
  __publicField(this, "queue", []);
36919
36902
  __publicField(this, "activeAssociations", 0);
@@ -36926,113 +36909,36 @@ var DicomSender = class _DicomSender extends EventEmitter {
36926
36909
  // Bucket state (bucket mode only)
36927
36910
  __publicField(this, "currentBucket", []);
36928
36911
  __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
- }
36912
+ this.config = config;
36913
+ this.effectiveMaxAssociations = config.configuredMaxAssociations;
36948
36914
  }
36949
36915
  // -----------------------------------------------------------------------
36950
36916
  // Public API
36951
36917
  // -----------------------------------------------------------------------
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) {
36918
+ /** Sends files through the engine's queue/bucket system. */
36919
+ send(files, timeoutMs, maxRetries, binaryParams) {
36981
36920
  if (this.isStopped) {
36982
- return Promise.resolve(err(new Error("DicomSender: sender is stopped")));
36921
+ return Promise.resolve(err(new Error(`${this.config.senderName}: sender is stopped`)));
36983
36922
  }
36984
36923
  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);
36924
+ return Promise.resolve(err(new Error(`${this.config.senderName}: no files provided`)));
37011
36925
  }
36926
+ return this.dispatchSend(files, timeoutMs, maxRetries, binaryParams);
37012
36927
  }
37013
- /**
37014
- * Flushes the current bucket immediately (bucket mode only).
37015
- * In single/multiple mode this is a no-op.
37016
- */
36928
+ /** Flushes the current bucket immediately (bucket mode only). */
37017
36929
  flush() {
37018
- if (this.mode !== "bucket") return;
36930
+ if (this.config.mode !== "bucket") return;
37019
36931
  if (this.currentBucket.length === 0) return;
37020
36932
  this.clearBucketTimer();
37021
36933
  this.flushBucketInternal("timer");
37022
36934
  }
37023
- /**
37024
- * Gracefully stops the sender. Rejects all queued items and
37025
- * waits for active associations to complete.
37026
- */
36935
+ /** Gracefully stops the engine. Rejects all queued items and waits for active associations. */
37027
36936
  async stop() {
37028
36937
  if (this.isStopped) return;
37029
36938
  this.isStopped = true;
37030
- if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
37031
- this.options.signal.removeEventListener("abort", this.abortHandler);
37032
- }
37033
36939
  this.clearBucketTimer();
37034
- this.rejectBucket("DicomSender: sender stopped");
37035
- this.rejectQueue("DicomSender: sender stopped");
36940
+ this.rejectBucket(`${this.config.senderName}: sender stopped`);
36941
+ this.rejectQueue(`${this.config.senderName}: sender stopped`);
37036
36942
  await this.waitForActive();
37037
36943
  }
37038
36944
  /** Current sender status. */
@@ -37047,77 +36953,37 @@ var DicomSender = class _DicomSender extends EventEmitter {
37047
36953
  stopped: this.isStopped
37048
36954
  };
37049
36955
  }
36956
+ /** Whether the engine has been stopped. */
36957
+ get stopped() {
36958
+ return this.isStopped;
36959
+ }
37050
36960
  // -----------------------------------------------------------------------
37051
- // Typed event listener convenience methods
36961
+ // Dispatch
37052
36962
  // -----------------------------------------------------------------------
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);
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
+ }
37098
36974
  }
37099
36975
  // -----------------------------------------------------------------------
37100
36976
  // Single/Multiple mode: queue-based dispatch
37101
36977
  // -----------------------------------------------------------------------
37102
36978
  /** Enqueues a send and dispatches immediately if capacity allows. */
37103
- enqueueSend(files, params) {
36979
+ enqueueSend(files, timeoutMs, maxRetries, binaryParams) {
37104
36980
  return new Promise((resolve) => {
37105
36981
  const totalQueued = this.queue.length + this.currentBucket.length;
37106
- if (totalQueued >= this.maxQueueLength) {
37107
- resolve(err(new Error("DicomSender: queue full")));
36982
+ if (totalQueued >= this.config.maxQueueLength) {
36983
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37108
36984
  return;
37109
36985
  }
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
- };
36986
+ const entry = { files, timeoutMs, maxRetries, binaryParams, resolve };
37121
36987
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37122
36988
  void this.executeEntry(entry);
37123
36989
  } else {
@@ -37137,27 +37003,16 @@ var DicomSender = class _DicomSender extends EventEmitter {
37137
37003
  // Bucket mode: accumulate-then-flush
37138
37004
  // -----------------------------------------------------------------------
37139
37005
  /** Adds files to the current bucket and triggers flush if full. */
37140
- enqueueBucket(files, params) {
37006
+ enqueueBucket(files, timeoutMs, maxRetries, binaryParams) {
37141
37007
  return new Promise((resolve) => {
37142
37008
  const totalQueued = this.queue.length + this.currentBucket.length;
37143
- if (totalQueued >= this.maxQueueLength) {
37144
- resolve(err(new Error("DicomSender: queue full")));
37009
+ if (totalQueued >= this.config.maxQueueLength) {
37010
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37145
37011
  return;
37146
37012
  }
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
- });
37013
+ this.currentBucket.push({ files, resolve, timeoutMs, maxRetries, binaryParams });
37159
37014
  const totalFiles = this.countBucketFiles();
37160
- if (totalFiles >= this.maxBucketSize) {
37015
+ if (totalFiles >= this.config.maxBucketSize) {
37161
37016
  this.clearBucketTimer();
37162
37017
  void this.flushBucketInternal("maxSize");
37163
37018
  } else {
@@ -37189,11 +37044,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37189
37044
  files: allFiles,
37190
37045
  timeoutMs,
37191
37046
  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,
37047
+ binaryParams: entries[0].binaryParams,
37197
37048
  resolve: (result) => {
37198
37049
  for (let i = 0; i < entries.length; i++) {
37199
37050
  entries[i].resolve(result);
@@ -37207,7 +37058,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37207
37058
  const entries = [...this.currentBucket];
37208
37059
  this.currentBucket = [];
37209
37060
  const bucketEntry = this.mergeBucketEntries(entries);
37210
- this.emit("BUCKET_FLUSHED", { fileCount: bucketEntry.files.length, reason });
37061
+ this.config.emitters.emitBucketFlushed({ fileCount: bucketEntry.files.length, reason });
37211
37062
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37212
37063
  void this.executeEntry(bucketEntry);
37213
37064
  } else {
@@ -37220,7 +37071,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37220
37071
  this.bucketTimer = setTimeout(() => {
37221
37072
  this.bucketTimer = void 0;
37222
37073
  void this.flushBucketInternal("timer");
37223
- }, this.bucketFlushMs);
37074
+ }, this.config.bucketFlushMs);
37224
37075
  }
37225
37076
  /** Clears the bucket flush timer. */
37226
37077
  clearBucketTimer() {
@@ -37232,7 +37083,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37232
37083
  // -----------------------------------------------------------------------
37233
37084
  // Core send execution with retry
37234
37085
  // -----------------------------------------------------------------------
37235
- /** Executes a single queue entry: calls storescu with retry. */
37086
+ /** Executes a single queue entry: calls the binary with retry. */
37236
37087
  async executeEntry(entry) {
37237
37088
  this.activeAssociations++;
37238
37089
  const startMs = Date.now();
@@ -37241,7 +37092,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37241
37092
  if (failure === void 0) return;
37242
37093
  this.activeAssociations--;
37243
37094
  this.recordFailure();
37244
- this.emit("SEND_FAILED", {
37095
+ this.config.emitters.emitSendFailed({
37245
37096
  files: entry.files,
37246
37097
  error: failure.error,
37247
37098
  attempts: maxAttempts,
@@ -37251,49 +37102,27 @@ var DicomSender = class _DicomSender extends EventEmitter {
37251
37102
  entry.resolve(err(failure.error));
37252
37103
  this.drainQueue();
37253
37104
  }
37254
- /** 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. */
37255
37106
  async attemptSend(entry, maxAttempts, startMs) {
37256
37107
  let lastError;
37257
37108
  const lastOutput = { stdout: "", stderr: "" };
37258
37109
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
37259
37110
  if (this.isStopped) {
37260
37111
  this.activeAssociations--;
37261
- entry.resolve(err(new Error("DicomSender: sender stopped")));
37112
+ entry.resolve(err(new Error(`${this.config.senderName}: sender stopped`)));
37262
37113
  return void 0;
37263
37114
  }
37264
- const result = await this.callStorescu(entry);
37115
+ const result = await this.config.executor(entry.files, entry.timeoutMs, entry.binaryParams, this.config.signal);
37265
37116
  if (result.ok) {
37266
37117
  this.handleSendSuccess(entry, startMs, result.value);
37267
37118
  return void 0;
37268
37119
  }
37269
37120
  lastError = result.error;
37270
37121
  if (attempt < maxAttempts - 1) {
37271
- await delay2(this.retryDelayMs * (attempt + 1));
37122
+ await delay2(this.config.retryDelayMs * (attempt + 1));
37272
37123
  }
37273
37124
  }
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
- });
37125
+ return { error: lastError ?? new Error(`${this.config.senderName}: send failed`), output: lastOutput };
37297
37126
  }
37298
37127
  /** Handles a successful send: updates state, emits event, resolves promise. */
37299
37128
  handleSendSuccess(entry, startMs, output) {
@@ -37301,7 +37130,7 @@ var DicomSender = class _DicomSender extends EventEmitter {
37301
37130
  const durationMs = Date.now() - startMs;
37302
37131
  this.recordSuccess();
37303
37132
  const data = { files: entry.files, fileCount: entry.files.length, durationMs, stdout: output.stdout, stderr: output.stderr };
37304
- this.emit("SEND_COMPLETE", data);
37133
+ this.config.emitters.emitSendComplete(data);
37305
37134
  entry.resolve(ok(data));
37306
37135
  this.drainQueue();
37307
37136
  }
@@ -37319,8 +37148,8 @@ var DicomSender = class _DicomSender extends EventEmitter {
37319
37148
  if (this.health === SenderHealth.DOWN) {
37320
37149
  this.health = SenderHealth.DEGRADED;
37321
37150
  } else {
37322
- this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.configuredMaxAssociations);
37323
- if (this.effectiveMaxAssociations >= this.configuredMaxAssociations) {
37151
+ this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.config.configuredMaxAssociations);
37152
+ if (this.effectiveMaxAssociations >= this.config.configuredMaxAssociations) {
37324
37153
  this.health = SenderHealth.HEALTHY;
37325
37154
  }
37326
37155
  }
@@ -37340,22 +37169,26 @@ var DicomSender = class _DicomSender extends EventEmitter {
37340
37169
  this.emitHealthChanged(previousHealth);
37341
37170
  }
37342
37171
  } 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));
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;
37346
37185
  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
37186
  }
37354
37187
  }
37355
37188
  }
37356
- /** Emits a HEALTH_CHANGED event. */
37189
+ /** Emits a HEALTH_CHANGED event via the wrapper's emitter. */
37357
37190
  emitHealthChanged(previousHealth) {
37358
- this.emit("HEALTH_CHANGED", {
37191
+ this.config.emitters.emitHealthChanged({
37359
37192
  previousHealth,
37360
37193
  newHealth: this.health,
37361
37194
  effectiveMaxAssociations: this.effectiveMaxAssociations,
@@ -37395,20 +37228,6 @@ var DicomSender = class _DicomSender extends EventEmitter {
37395
37228
  check();
37396
37229
  });
37397
37230
  }
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
37231
  };
37413
37232
  function delay2(ms) {
37414
37233
  return new Promise((resolve) => {
@@ -37416,6 +37235,492 @@ function delay2(ms) {
37416
37235
  });
37417
37236
  }
37418
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
+
37419
37724
  // src/pacs/types.ts
37420
37725
  var QueryLevel = {
37421
37726
  STUDY: "STUDY",
@@ -37965,7 +38270,7 @@ var DEFAULT_CONFIG = {
37965
38270
  signal: void 0,
37966
38271
  onRetry: void 0
37967
38272
  };
37968
- function resolveConfig2(opts) {
38273
+ function resolveConfig3(opts) {
37969
38274
  if (!opts) return DEFAULT_CONFIG;
37970
38275
  return {
37971
38276
  ...DEFAULT_CONFIG,
@@ -38010,7 +38315,7 @@ function shouldBreakAfterFailure(attempt, lastError, config) {
38010
38315
  return false;
38011
38316
  }
38012
38317
  async function retry(operation, options) {
38013
- const config = resolveConfig2(options);
38318
+ const config = resolveConfig3(options);
38014
38319
  let lastResult = err(new Error("No attempts executed"));
38015
38320
  for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
38016
38321
  lastResult = await operation();
@@ -38024,6 +38329,6 @@ async function retry(operation, options) {
38024
38329
  return lastResult;
38025
38330
  }
38026
38331
 
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 };
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 };
38028
38333
  //# sourceMappingURL=index.js.map
38029
38334
  //# sourceMappingURL=index.js.map