@ubercode/dcmtk 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -33131,6 +33131,30 @@ async function echoscu(options) {
33131
33131
  return ok({ success: true, stderr: result.value.stderr });
33132
33132
  }
33133
33133
  var VERBOSITY_FLAGS19 = { verbose: "-v", debug: "-d" };
33134
+ var DECOMPRESS_FLAGS = {
33135
+ never: "--decompress-never",
33136
+ lossless: "--decompress-lossless",
33137
+ lossy: "--decompress-lossy"
33138
+ };
33139
+ function pushSendModeArgs(args, options) {
33140
+ if (options.noHalt === true) {
33141
+ args.push("--no-halt");
33142
+ }
33143
+ if (options.noIllegalProposal === true) {
33144
+ args.push("--no-illegal-proposal");
33145
+ }
33146
+ if (options.decompress !== void 0) {
33147
+ args.push(DECOMPRESS_FLAGS[options.decompress]);
33148
+ }
33149
+ if (options.multiAssociations === true) {
33150
+ args.push("+ma");
33151
+ } else if (options.multiAssociations === false) {
33152
+ args.push("-ma");
33153
+ }
33154
+ if (options.createReportFile !== void 0) {
33155
+ args.push("--create-report-file", options.createReportFile);
33156
+ }
33157
+ }
33134
33158
  var DcmsendOptionsSchema = zod.z.object({
33135
33159
  timeoutMs: zod.z.number().int().positive().optional(),
33136
33160
  signal: zod.z.instanceof(AbortSignal).optional(),
@@ -33140,8 +33164,15 @@ var DcmsendOptionsSchema = zod.z.object({
33140
33164
  callingAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
33141
33165
  calledAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
33142
33166
  scanDirectory: zod.z.boolean().optional(),
33167
+ recurse: zod.z.boolean().optional(),
33168
+ scanPattern: zod.z.string().min(1).optional(),
33143
33169
  verbosity: zod.z.enum(["verbose", "debug"]).optional(),
33144
33170
  noUidChecks: zod.z.boolean().optional(),
33171
+ noHalt: zod.z.boolean().optional(),
33172
+ noIllegalProposal: zod.z.boolean().optional(),
33173
+ decompress: zod.z.enum(["never", "lossless", "lossy"]).optional(),
33174
+ multiAssociations: zod.z.boolean().optional(),
33175
+ createReportFile: zod.z.string().min(1).optional(),
33145
33176
  maxPduReceive: zod.z.number().int().min(4096).max(131072).optional(),
33146
33177
  maxPduSend: zod.z.number().int().min(4096).max(131072).optional(),
33147
33178
  noHostnameLookup: zod.z.boolean().optional(),
@@ -33187,6 +33218,13 @@ function buildArgs33(options) {
33187
33218
  if (options.scanDirectory === true) {
33188
33219
  args.push("--scan-directories");
33189
33220
  }
33221
+ if (options.recurse === true) {
33222
+ args.push("+r");
33223
+ }
33224
+ if (options.scanPattern !== void 0) {
33225
+ args.push("--scan-pattern", options.scanPattern);
33226
+ }
33227
+ pushSendModeArgs(args, options);
33190
33228
  args.push(options.host, String(options.port));
33191
33229
  args.push(...options.files);
33192
33230
  return args;
@@ -33225,7 +33263,18 @@ var ProposedTransferSyntax = {
33225
33263
  J2K_LOSSLESS: "j2kLossless",
33226
33264
  J2K_LOSSY: "j2kLossy",
33227
33265
  JLS_LOSSLESS: "jlsLossless",
33228
- JLS_LOSSY: "jlsLossy"
33266
+ JLS_LOSSY: "jlsLossy",
33267
+ MPEG2: "mpeg2",
33268
+ MPEG2_HIGH: "mpeg2High",
33269
+ MPEG4: "mpeg4",
33270
+ MPEG4_BD: "mpeg4Bd",
33271
+ MPEG4_2_2D: "mpeg4_2_2d",
33272
+ MPEG4_2_3D: "mpeg4_2_3d",
33273
+ MPEG4_2_ST: "mpeg4_2_st",
33274
+ HEVC: "hevc",
33275
+ HEVC10: "hevc10",
33276
+ RLE: "rle",
33277
+ DEFLATED: "deflated"
33229
33278
  };
33230
33279
  var PROPOSED_TS_FLAG_MAP = {
33231
33280
  [ProposedTransferSyntax.UNCOMPRESSED]: "-x=",
@@ -33238,8 +33287,20 @@ var PROPOSED_TS_FLAG_MAP = {
33238
33287
  [ProposedTransferSyntax.J2K_LOSSLESS]: "-xv",
33239
33288
  [ProposedTransferSyntax.J2K_LOSSY]: "-xw",
33240
33289
  [ProposedTransferSyntax.JLS_LOSSLESS]: "-xt",
33241
- [ProposedTransferSyntax.JLS_LOSSY]: "-xu"
33290
+ [ProposedTransferSyntax.JLS_LOSSY]: "-xu",
33291
+ [ProposedTransferSyntax.MPEG2]: "-xm",
33292
+ [ProposedTransferSyntax.MPEG2_HIGH]: "-xh",
33293
+ [ProposedTransferSyntax.MPEG4]: "-xn",
33294
+ [ProposedTransferSyntax.MPEG4_BD]: "-xl",
33295
+ [ProposedTransferSyntax.MPEG4_2_2D]: "-x2",
33296
+ [ProposedTransferSyntax.MPEG4_2_3D]: "-x3",
33297
+ [ProposedTransferSyntax.MPEG4_2_ST]: "-xo",
33298
+ [ProposedTransferSyntax.HEVC]: "-x4",
33299
+ [ProposedTransferSyntax.HEVC10]: "-x5",
33300
+ [ProposedTransferSyntax.RLE]: "-xr",
33301
+ [ProposedTransferSyntax.DEFLATED]: "-xd"
33242
33302
  };
33303
+ var PROPOSED_TS_VALUES = Object.values(ProposedTransferSyntax);
33243
33304
  var StorescuOptionsSchema = zod.z.object({
33244
33305
  timeoutMs: zod.z.number().int().positive().optional(),
33245
33306
  signal: zod.z.instanceof(AbortSignal).optional(),
@@ -33258,20 +33319,18 @@ var StorescuOptionsSchema = zod.z.object({
33258
33319
  dimseTimeout: zod.z.number().int().positive().optional(),
33259
33320
  noHostnameLookup: zod.z.boolean().optional(),
33260
33321
  required: zod.z.boolean().optional(),
33261
- proposedTransferSyntax: zod.z.enum([
33262
- "uncompressed",
33263
- "littleEndian",
33264
- "bigEndian",
33265
- "implicitVR",
33266
- "jpegLossless",
33267
- "jpeg8Bit",
33268
- "jpeg12Bit",
33269
- "j2kLossless",
33270
- "j2kLossy",
33271
- "jlsLossless",
33272
- "jlsLossy"
33273
- ]).optional()
33322
+ proposedTransferSyntax: zod.z.union([zod.z.enum(PROPOSED_TS_VALUES), zod.z.array(zod.z.enum(PROPOSED_TS_VALUES)).min(1)]).optional(),
33323
+ combineProposedTransferSyntaxes: zod.z.boolean().optional()
33274
33324
  }).strict();
33325
+ function pushProposedTsArgs(args, ts) {
33326
+ if (typeof ts === "string") {
33327
+ args.push(PROPOSED_TS_FLAG_MAP[ts]);
33328
+ } else {
33329
+ for (let i = 0; i < ts.length; i++) {
33330
+ args.push(PROPOSED_TS_FLAG_MAP[ts[i]]);
33331
+ }
33332
+ }
33333
+ }
33275
33334
  var VERBOSITY_FLAGS20 = { verbose: "-v", debug: "-d" };
33276
33335
  var DIMSE_ERROR_PATTERN = /^E:.*(?:DIMSE Failed|Store Failed)/m;
33277
33336
  function pushNetworkArgs3(args, options) {
@@ -33313,7 +33372,10 @@ function buildArgs34(options) {
33313
33372
  args.push("+r");
33314
33373
  }
33315
33374
  if (options.proposedTransferSyntax !== void 0) {
33316
- args.push(PROPOSED_TS_FLAG_MAP[options.proposedTransferSyntax]);
33375
+ pushProposedTsArgs(args, options.proposedTransferSyntax);
33376
+ }
33377
+ if (options.combineProposedTransferSyntaxes === true) {
33378
+ args.push("+C");
33317
33379
  }
33318
33380
  if (options.required === true) {
33319
33381
  args.push("-R");
@@ -36853,79 +36915,13 @@ var SenderHealth = {
36853
36915
  DOWN: "down"
36854
36916
  };
36855
36917
 
36856
- // src/senders/DicomSender.ts
36857
- var DEFAULT_MAX_ASSOCIATIONS = 4;
36858
- var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
36859
- var DEFAULT_MAX_RETRIES = 3;
36860
- var DEFAULT_RETRY_DELAY_MS = 1e3;
36861
- var DEFAULT_BUCKET_FLUSH_MS = 5e3;
36862
- var DEFAULT_MAX_BUCKET_SIZE = 50;
36863
- var MAX_ASSOCIATIONS_LIMIT = 64;
36918
+ // src/senders/SenderEngine.ts
36864
36919
  var DEGRADE_THRESHOLD = 3;
36865
36920
  var DOWN_THRESHOLD = 10;
36866
36921
  var RECOVERY_THRESHOLD = 3;
36867
- var DicomSenderOptionsSchema = zod.z.object({
36868
- host: zod.z.string().min(1),
36869
- port: zod.z.number().int().min(1).max(65535),
36870
- calledAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
36871
- callingAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
36872
- mode: zod.z.enum(["single", "multiple", "bucket"]).optional(),
36873
- maxAssociations: zod.z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
36874
- proposedTransferSyntax: zod.z.enum([
36875
- "uncompressed",
36876
- "littleEndian",
36877
- "bigEndian",
36878
- "implicitVR",
36879
- "jpegLossless",
36880
- "jpeg8Bit",
36881
- "jpeg12Bit",
36882
- "j2kLossless",
36883
- "j2kLossy",
36884
- "jlsLossless",
36885
- "jlsLossy"
36886
- ]).optional(),
36887
- maxQueueLength: zod.z.number().int().min(1).optional(),
36888
- timeoutMs: zod.z.number().int().positive().optional(),
36889
- maxRetries: zod.z.number().int().min(0).optional(),
36890
- retryDelayMs: zod.z.number().int().min(0).optional(),
36891
- bucketFlushMs: zod.z.number().int().positive().optional(),
36892
- maxBucketSize: zod.z.number().int().min(1).optional(),
36893
- maxPduReceive: zod.z.number().int().min(4096).max(131072).optional(),
36894
- maxPduSend: zod.z.number().int().min(4096).max(131072).optional(),
36895
- associationTimeout: zod.z.number().int().positive().optional(),
36896
- acseTimeout: zod.z.number().int().positive().optional(),
36897
- dimseTimeout: zod.z.number().int().positive().optional(),
36898
- noHostnameLookup: zod.z.boolean().optional(),
36899
- verbosity: zod.z.enum(["verbose", "debug"]).optional(),
36900
- required: zod.z.boolean().optional(),
36901
- signal: zod.z.instanceof(AbortSignal).optional()
36902
- }).strict();
36903
- function resolveConfig(options) {
36904
- const mode = options.mode ?? "multiple";
36905
- const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
36906
- return {
36907
- mode,
36908
- configuredMaxAssociations: mode === "single" ? 1 : rawMax,
36909
- maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
36910
- defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
36911
- defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
36912
- retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
36913
- bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
36914
- maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
36915
- };
36916
- }
36917
- var DicomSender = class _DicomSender extends events.EventEmitter {
36918
- constructor(options) {
36919
- super();
36920
- __publicField(this, "options");
36921
- __publicField(this, "mode");
36922
- __publicField(this, "configuredMaxAssociations");
36923
- __publicField(this, "maxQueueLength");
36924
- __publicField(this, "defaultTimeoutMs");
36925
- __publicField(this, "defaultMaxRetries");
36926
- __publicField(this, "retryDelayMs");
36927
- __publicField(this, "bucketFlushMs");
36928
- __publicField(this, "maxBucketSize");
36922
+ var SenderEngine = class {
36923
+ constructor(config) {
36924
+ __publicField(this, "config");
36929
36925
  // Queue and concurrency state
36930
36926
  __publicField(this, "queue", []);
36931
36927
  __publicField(this, "activeAssociations", 0);
@@ -36938,108 +36934,36 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
36938
36934
  // Bucket state (bucket mode only)
36939
36935
  __publicField(this, "currentBucket", []);
36940
36936
  __publicField(this, "bucketTimer");
36941
- // AbortSignal
36942
- __publicField(this, "abortHandler");
36943
- this.setMaxListeners(20);
36944
- this.on("error", () => {
36945
- });
36946
- this.options = options;
36947
- const cfg = resolveConfig(options);
36948
- this.mode = cfg.mode;
36949
- this.configuredMaxAssociations = cfg.configuredMaxAssociations;
36950
- this.effectiveMaxAssociations = cfg.configuredMaxAssociations;
36951
- this.maxQueueLength = cfg.maxQueueLength;
36952
- this.defaultTimeoutMs = cfg.defaultTimeoutMs;
36953
- this.defaultMaxRetries = cfg.defaultMaxRetries;
36954
- this.retryDelayMs = cfg.retryDelayMs;
36955
- this.bucketFlushMs = cfg.bucketFlushMs;
36956
- this.maxBucketSize = cfg.maxBucketSize;
36957
- if (options.signal !== void 0) {
36958
- this.wireAbortSignal(options.signal);
36959
- }
36937
+ this.config = config;
36938
+ this.effectiveMaxAssociations = config.configuredMaxAssociations;
36960
36939
  }
36961
36940
  // -----------------------------------------------------------------------
36962
36941
  // Public API
36963
36942
  // -----------------------------------------------------------------------
36964
- /**
36965
- * Creates a new DicomSender instance.
36966
- *
36967
- * @param options - Configuration options
36968
- * @returns A Result containing the instance or a validation error
36969
- */
36970
- static create(options) {
36971
- const validation = DicomSenderOptionsSchema.safeParse(options);
36972
- if (!validation.success) {
36973
- return err(createValidationError("DicomSender", validation.error));
36974
- }
36975
- return ok(new _DicomSender(options));
36976
- }
36977
- /**
36978
- * Sends one or more DICOM files to the remote endpoint.
36979
- *
36980
- * In single/multiple mode, files are sent as one storescu call.
36981
- * In bucket mode, files are accumulated into a bucket and flushed
36982
- * when the bucket reaches maxBucketSize or the flush timer fires.
36983
- *
36984
- * The returned promise resolves when the files are actually sent
36985
- * (not just queued). Callers can await for confirmation or
36986
- * fire-and-forget with `void sender.send(files)`.
36987
- *
36988
- * @param files - One or more DICOM file paths
36989
- * @param options - Per-send overrides
36990
- * @returns A Result containing the send result or an error
36991
- */
36992
- send(files, options) {
36943
+ /** Sends files through the engine's queue/bucket system. */
36944
+ send(files, timeoutMs, maxRetries, binaryParams) {
36993
36945
  if (this.isStopped) {
36994
- return Promise.resolve(err(new Error("DicomSender: sender is stopped")));
36946
+ return Promise.resolve(err(new Error(`${this.config.senderName}: sender is stopped`)));
36995
36947
  }
36996
36948
  if (files.length === 0) {
36997
- return Promise.resolve(err(new Error("DicomSender: no files provided")));
36998
- }
36999
- const params = {
37000
- timeoutMs: options?.timeoutMs ?? this.defaultTimeoutMs,
37001
- maxRetries: options?.maxRetries ?? this.defaultMaxRetries,
37002
- calledAETitle: options?.calledAETitle,
37003
- callingAETitle: options?.callingAETitle,
37004
- required: options?.required
37005
- };
37006
- return this.dispatchSend(files, params);
37007
- }
37008
- /** Dispatches a send to the appropriate mode handler. */
37009
- dispatchSend(files, params) {
37010
- switch (this.mode) {
37011
- case "single":
37012
- case "multiple":
37013
- return this.enqueueSend(files, params);
37014
- case "bucket":
37015
- return this.enqueueBucket(files, params);
37016
- default:
37017
- assertUnreachable(this.mode);
36949
+ return Promise.resolve(err(new Error(`${this.config.senderName}: no files provided`)));
37018
36950
  }
36951
+ return this.dispatchSend(files, timeoutMs, maxRetries, binaryParams);
37019
36952
  }
37020
- /**
37021
- * Flushes the current bucket immediately (bucket mode only).
37022
- * In single/multiple mode this is a no-op.
37023
- */
36953
+ /** Flushes the current bucket immediately (bucket mode only). */
37024
36954
  flush() {
37025
- if (this.mode !== "bucket") return;
36955
+ if (this.config.mode !== "bucket") return;
37026
36956
  if (this.currentBucket.length === 0) return;
37027
36957
  this.clearBucketTimer();
37028
36958
  this.flushBucketInternal("timer");
37029
36959
  }
37030
- /**
37031
- * Gracefully stops the sender. Rejects all queued items and
37032
- * waits for active associations to complete.
37033
- */
36960
+ /** Gracefully stops the engine. Rejects all queued items and waits for active associations. */
37034
36961
  async stop() {
37035
36962
  if (this.isStopped) return;
37036
36963
  this.isStopped = true;
37037
- if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
37038
- this.options.signal.removeEventListener("abort", this.abortHandler);
37039
- }
37040
36964
  this.clearBucketTimer();
37041
- this.rejectBucket("DicomSender: sender stopped");
37042
- this.rejectQueue("DicomSender: sender stopped");
36965
+ this.rejectBucket(`${this.config.senderName}: sender stopped`);
36966
+ this.rejectQueue(`${this.config.senderName}: sender stopped`);
37043
36967
  await this.waitForActive();
37044
36968
  }
37045
36969
  /** Current sender status. */
@@ -37054,75 +36978,37 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37054
36978
  stopped: this.isStopped
37055
36979
  };
37056
36980
  }
36981
+ /** Whether the engine has been stopped. */
36982
+ get stopped() {
36983
+ return this.isStopped;
36984
+ }
37057
36985
  // -----------------------------------------------------------------------
37058
- // Typed event listener convenience methods
36986
+ // Dispatch
37059
36987
  // -----------------------------------------------------------------------
37060
- /**
37061
- * Registers a typed listener for a DicomSender-specific event.
37062
- *
37063
- * @param event - The event name from DicomSenderEventMap
37064
- * @param listener - Callback receiving typed event data
37065
- * @returns this for chaining
37066
- */
37067
- onEvent(event, listener) {
37068
- return this.on(event, listener);
37069
- }
37070
- /**
37071
- * Registers a listener for successful sends.
37072
- *
37073
- * @param listener - Callback receiving send complete data
37074
- * @returns this for chaining
37075
- */
37076
- onSendComplete(listener) {
37077
- return this.on("SEND_COMPLETE", listener);
37078
- }
37079
- /**
37080
- * Registers a listener for failed sends.
37081
- *
37082
- * @param listener - Callback receiving send failed data
37083
- * @returns this for chaining
37084
- */
37085
- onSendFailed(listener) {
37086
- return this.on("SEND_FAILED", listener);
37087
- }
37088
- /**
37089
- * Registers a listener for health state changes.
37090
- *
37091
- * @param listener - Callback receiving health change data
37092
- * @returns this for chaining
37093
- */
37094
- onHealthChanged(listener) {
37095
- return this.on("HEALTH_CHANGED", listener);
37096
- }
37097
- /**
37098
- * Registers a listener for bucket flushes (bucket mode only).
37099
- *
37100
- * @param listener - Callback receiving bucket flush data
37101
- * @returns this for chaining
37102
- */
37103
- onBucketFlushed(listener) {
37104
- return this.on("BUCKET_FLUSHED", listener);
36988
+ /** Dispatches a send to the appropriate mode handler. */
36989
+ dispatchSend(files, timeoutMs, maxRetries, binaryParams) {
36990
+ switch (this.config.mode) {
36991
+ case "single":
36992
+ case "multiple":
36993
+ return this.enqueueSend(files, timeoutMs, maxRetries, binaryParams);
36994
+ case "bucket":
36995
+ return this.enqueueBucket(files, timeoutMs, maxRetries, binaryParams);
36996
+ default:
36997
+ assertUnreachable(this.config.mode);
36998
+ }
37105
36999
  }
37106
37000
  // -----------------------------------------------------------------------
37107
37001
  // Single/Multiple mode: queue-based dispatch
37108
37002
  // -----------------------------------------------------------------------
37109
37003
  /** Enqueues a send and dispatches immediately if capacity allows. */
37110
- enqueueSend(files, params) {
37004
+ enqueueSend(files, timeoutMs, maxRetries, binaryParams) {
37111
37005
  return new Promise((resolve) => {
37112
37006
  const totalQueued = this.queue.length + this.currentBucket.length;
37113
- if (totalQueued >= this.maxQueueLength) {
37114
- resolve(err(new Error("DicomSender: queue full")));
37007
+ if (totalQueued >= this.config.maxQueueLength) {
37008
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37115
37009
  return;
37116
37010
  }
37117
- const entry = {
37118
- files,
37119
- timeoutMs: params.timeoutMs,
37120
- maxRetries: params.maxRetries,
37121
- calledAETitle: params.calledAETitle,
37122
- callingAETitle: params.callingAETitle,
37123
- required: params.required,
37124
- resolve
37125
- };
37011
+ const entry = { files, timeoutMs, maxRetries, binaryParams, resolve };
37126
37012
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37127
37013
  void this.executeEntry(entry);
37128
37014
  } else {
@@ -37142,17 +37028,16 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37142
37028
  // Bucket mode: accumulate-then-flush
37143
37029
  // -----------------------------------------------------------------------
37144
37030
  /** Adds files to the current bucket and triggers flush if full. */
37145
- enqueueBucket(files, params) {
37031
+ enqueueBucket(files, timeoutMs, maxRetries, binaryParams) {
37146
37032
  return new Promise((resolve) => {
37147
37033
  const totalQueued = this.queue.length + this.currentBucket.length;
37148
- if (totalQueued >= this.maxQueueLength) {
37149
- resolve(err(new Error("DicomSender: queue full")));
37034
+ if (totalQueued >= this.config.maxQueueLength) {
37035
+ resolve(err(new Error(`${this.config.senderName}: queue full`)));
37150
37036
  return;
37151
37037
  }
37152
- const { timeoutMs, maxRetries, calledAETitle, callingAETitle, required } = params;
37153
- this.currentBucket.push({ files, resolve, timeoutMs, maxRetries, calledAETitle, callingAETitle, required });
37038
+ this.currentBucket.push({ files, resolve, timeoutMs, maxRetries, binaryParams });
37154
37039
  const totalFiles = this.countBucketFiles();
37155
- if (totalFiles >= this.maxBucketSize) {
37040
+ if (totalFiles >= this.config.maxBucketSize) {
37156
37041
  this.clearBucketTimer();
37157
37042
  void this.flushBucketInternal("maxSize");
37158
37043
  } else {
@@ -37184,9 +37069,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37184
37069
  files: allFiles,
37185
37070
  timeoutMs,
37186
37071
  maxRetries,
37187
- calledAETitle: entries[0]?.calledAETitle,
37188
- callingAETitle: entries[0]?.callingAETitle,
37189
- required: entries[0]?.required,
37072
+ binaryParams: entries[0].binaryParams,
37190
37073
  resolve: (result) => {
37191
37074
  for (let i = 0; i < entries.length; i++) {
37192
37075
  entries[i].resolve(result);
@@ -37200,7 +37083,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37200
37083
  const entries = [...this.currentBucket];
37201
37084
  this.currentBucket = [];
37202
37085
  const bucketEntry = this.mergeBucketEntries(entries);
37203
- this.emit("BUCKET_FLUSHED", { fileCount: bucketEntry.files.length, reason });
37086
+ this.config.emitters.emitBucketFlushed({ fileCount: bucketEntry.files.length, reason });
37204
37087
  if (this.activeAssociations < this.effectiveMaxAssociations) {
37205
37088
  void this.executeEntry(bucketEntry);
37206
37089
  } else {
@@ -37213,7 +37096,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37213
37096
  this.bucketTimer = setTimeout(() => {
37214
37097
  this.bucketTimer = void 0;
37215
37098
  void this.flushBucketInternal("timer");
37216
- }, this.bucketFlushMs);
37099
+ }, this.config.bucketFlushMs);
37217
37100
  }
37218
37101
  /** Clears the bucket flush timer. */
37219
37102
  clearBucketTimer() {
@@ -37225,7 +37108,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37225
37108
  // -----------------------------------------------------------------------
37226
37109
  // Core send execution with retry
37227
37110
  // -----------------------------------------------------------------------
37228
- /** Executes a single queue entry: calls storescu with retry. */
37111
+ /** Executes a single queue entry: calls the binary with retry. */
37229
37112
  async executeEntry(entry) {
37230
37113
  this.activeAssociations++;
37231
37114
  const startMs = Date.now();
@@ -37234,7 +37117,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37234
37117
  if (failure === void 0) return;
37235
37118
  this.activeAssociations--;
37236
37119
  this.recordFailure();
37237
- this.emit("SEND_FAILED", {
37120
+ this.config.emitters.emitSendFailed({
37238
37121
  files: entry.files,
37239
37122
  error: failure.error,
37240
37123
  attempts: maxAttempts,
@@ -37244,48 +37127,27 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37244
37127
  entry.resolve(err(failure.error));
37245
37128
  this.drainQueue();
37246
37129
  }
37247
- /** Attempts storescu up to maxAttempts times. Returns undefined on success, or failure info. */
37130
+ /** Attempts the binary call up to maxAttempts times. Returns undefined on success. */
37248
37131
  async attemptSend(entry, maxAttempts, startMs) {
37249
37132
  let lastError;
37250
37133
  const lastOutput = { stdout: "", stderr: "" };
37251
37134
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
37252
37135
  if (this.isStopped) {
37253
37136
  this.activeAssociations--;
37254
- entry.resolve(err(new Error("DicomSender: sender stopped")));
37137
+ entry.resolve(err(new Error(`${this.config.senderName}: sender stopped`)));
37255
37138
  return void 0;
37256
37139
  }
37257
- const result = await this.callStorescu(entry);
37140
+ const result = await this.config.executor(entry.files, entry.timeoutMs, entry.binaryParams, this.config.signal);
37258
37141
  if (result.ok) {
37259
37142
  this.handleSendSuccess(entry, startMs, result.value);
37260
37143
  return void 0;
37261
37144
  }
37262
37145
  lastError = result.error;
37263
37146
  if (attempt < maxAttempts - 1) {
37264
- await delay2(this.retryDelayMs * (attempt + 1));
37147
+ await delay2(this.config.retryDelayMs * (attempt + 1));
37265
37148
  }
37266
37149
  }
37267
- return { error: lastError ?? new Error("DicomSender: send failed"), output: lastOutput };
37268
- }
37269
- /** Calls storescu with the configured options. */
37270
- callStorescu(entry) {
37271
- return storescu({
37272
- host: this.options.host,
37273
- port: this.options.port,
37274
- files: [...entry.files],
37275
- calledAETitle: entry.calledAETitle ?? this.options.calledAETitle,
37276
- callingAETitle: entry.callingAETitle ?? this.options.callingAETitle,
37277
- proposedTransferSyntax: this.options.proposedTransferSyntax,
37278
- maxPduReceive: this.options.maxPduReceive,
37279
- maxPduSend: this.options.maxPduSend,
37280
- associationTimeout: this.options.associationTimeout,
37281
- acseTimeout: this.options.acseTimeout,
37282
- dimseTimeout: this.options.dimseTimeout,
37283
- noHostnameLookup: this.options.noHostnameLookup,
37284
- verbosity: this.options.verbosity,
37285
- required: entry.required ?? this.options.required,
37286
- timeoutMs: entry.timeoutMs,
37287
- signal: this.options.signal
37288
- });
37150
+ return { error: lastError ?? new Error(`${this.config.senderName}: send failed`), output: lastOutput };
37289
37151
  }
37290
37152
  /** Handles a successful send: updates state, emits event, resolves promise. */
37291
37153
  handleSendSuccess(entry, startMs, output) {
@@ -37293,7 +37155,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37293
37155
  const durationMs = Date.now() - startMs;
37294
37156
  this.recordSuccess();
37295
37157
  const data = { files: entry.files, fileCount: entry.files.length, durationMs, stdout: output.stdout, stderr: output.stderr };
37296
- this.emit("SEND_COMPLETE", data);
37158
+ this.config.emitters.emitSendComplete(data);
37297
37159
  entry.resolve(ok(data));
37298
37160
  this.drainQueue();
37299
37161
  }
@@ -37311,8 +37173,8 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37311
37173
  if (this.health === SenderHealth.DOWN) {
37312
37174
  this.health = SenderHealth.DEGRADED;
37313
37175
  } else {
37314
- this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.configuredMaxAssociations);
37315
- if (this.effectiveMaxAssociations >= this.configuredMaxAssociations) {
37176
+ this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.config.configuredMaxAssociations);
37177
+ if (this.effectiveMaxAssociations >= this.config.configuredMaxAssociations) {
37316
37178
  this.health = SenderHealth.HEALTHY;
37317
37179
  }
37318
37180
  }
@@ -37332,22 +37194,26 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37332
37194
  this.emitHealthChanged(previousHealth);
37333
37195
  }
37334
37196
  } else if (this.consecutiveFailures >= DEGRADE_THRESHOLD && this.consecutiveFailures % DEGRADE_THRESHOLD === 0) {
37335
- if (this.health === SenderHealth.HEALTHY) {
37336
- this.health = SenderHealth.DEGRADED;
37337
- this.effectiveMaxAssociations = Math.max(1, Math.floor(this.configuredMaxAssociations / 2));
37197
+ this.degradeHealth(previousHealth);
37198
+ }
37199
+ }
37200
+ /** Handles HEALTHY→DEGRADED or DEGRADED→DEGRADED transitions. */
37201
+ degradeHealth(previousHealth) {
37202
+ if (this.health === SenderHealth.HEALTHY) {
37203
+ this.health = SenderHealth.DEGRADED;
37204
+ this.effectiveMaxAssociations = Math.max(1, Math.floor(this.config.configuredMaxAssociations / 2));
37205
+ this.emitHealthChanged(previousHealth);
37206
+ } else if (this.health === SenderHealth.DEGRADED) {
37207
+ const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
37208
+ if (newMax !== this.effectiveMaxAssociations) {
37209
+ this.effectiveMaxAssociations = newMax;
37338
37210
  this.emitHealthChanged(previousHealth);
37339
- } else if (this.health === SenderHealth.DEGRADED) {
37340
- const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
37341
- if (newMax !== this.effectiveMaxAssociations) {
37342
- this.effectiveMaxAssociations = newMax;
37343
- this.emitHealthChanged(previousHealth);
37344
- }
37345
37211
  }
37346
37212
  }
37347
37213
  }
37348
- /** Emits a HEALTH_CHANGED event. */
37214
+ /** Emits a HEALTH_CHANGED event via the wrapper's emitter. */
37349
37215
  emitHealthChanged(previousHealth) {
37350
- this.emit("HEALTH_CHANGED", {
37216
+ this.config.emitters.emitHealthChanged({
37351
37217
  previousHealth,
37352
37218
  newHealth: this.health,
37353
37219
  effectiveMaxAssociations: this.effectiveMaxAssociations,
@@ -37387,20 +37253,6 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
37387
37253
  check();
37388
37254
  });
37389
37255
  }
37390
- // -----------------------------------------------------------------------
37391
- // Abort signal
37392
- // -----------------------------------------------------------------------
37393
- /** Wires an AbortSignal to stop the sender. */
37394
- wireAbortSignal(signal) {
37395
- if (signal.aborted) {
37396
- void this.stop();
37397
- return;
37398
- }
37399
- this.abortHandler = () => {
37400
- void this.stop();
37401
- };
37402
- signal.addEventListener("abort", this.abortHandler, { once: true });
37403
- }
37404
37256
  };
37405
37257
  function delay2(ms) {
37406
37258
  return new Promise((resolve) => {
@@ -37408,6 +37260,492 @@ function delay2(ms) {
37408
37260
  });
37409
37261
  }
37410
37262
 
37263
+ // src/senders/DicomSender.ts
37264
+ var DEFAULT_MAX_ASSOCIATIONS = 4;
37265
+ var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
37266
+ var DEFAULT_MAX_RETRIES = 3;
37267
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
37268
+ var DEFAULT_BUCKET_FLUSH_MS = 5e3;
37269
+ var DEFAULT_MAX_BUCKET_SIZE = 50;
37270
+ var MAX_ASSOCIATIONS_LIMIT = 64;
37271
+ var DicomSenderOptionsSchema = zod.z.object({
37272
+ host: zod.z.string().min(1),
37273
+ port: zod.z.number().int().min(1).max(65535),
37274
+ calledAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37275
+ callingAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37276
+ mode: zod.z.enum(["single", "multiple", "bucket"]).optional(),
37277
+ maxAssociations: zod.z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
37278
+ proposedTransferSyntax: zod.z.union([zod.z.enum(PROPOSED_TS_VALUES), zod.z.array(zod.z.enum(PROPOSED_TS_VALUES)).min(1)]).optional(),
37279
+ combineProposedTransferSyntaxes: zod.z.boolean().optional(),
37280
+ maxQueueLength: zod.z.number().int().min(1).optional(),
37281
+ timeoutMs: zod.z.number().int().positive().optional(),
37282
+ maxRetries: zod.z.number().int().min(0).optional(),
37283
+ retryDelayMs: zod.z.number().int().min(0).optional(),
37284
+ bucketFlushMs: zod.z.number().int().positive().optional(),
37285
+ maxBucketSize: zod.z.number().int().min(1).optional(),
37286
+ maxPduReceive: zod.z.number().int().min(4096).max(131072).optional(),
37287
+ maxPduSend: zod.z.number().int().min(4096).max(131072).optional(),
37288
+ associationTimeout: zod.z.number().int().positive().optional(),
37289
+ acseTimeout: zod.z.number().int().positive().optional(),
37290
+ dimseTimeout: zod.z.number().int().positive().optional(),
37291
+ noHostnameLookup: zod.z.boolean().optional(),
37292
+ verbosity: zod.z.enum(["verbose", "debug"]).optional(),
37293
+ required: zod.z.boolean().optional(),
37294
+ signal: zod.z.instanceof(AbortSignal).optional()
37295
+ }).strict();
37296
+ function createStorescuExecutor(options) {
37297
+ return async (files, timeoutMs, params, signal) => {
37298
+ const result = await storescu({
37299
+ host: options.host,
37300
+ port: options.port,
37301
+ files: [...files],
37302
+ calledAETitle: params.calledAETitle ?? options.calledAETitle,
37303
+ callingAETitle: params.callingAETitle ?? options.callingAETitle,
37304
+ proposedTransferSyntax: params.proposedTransferSyntax ?? options.proposedTransferSyntax,
37305
+ combineProposedTransferSyntaxes: params.combineProposedTransferSyntaxes ?? options.combineProposedTransferSyntaxes,
37306
+ maxPduReceive: options.maxPduReceive,
37307
+ maxPduSend: options.maxPduSend,
37308
+ associationTimeout: options.associationTimeout,
37309
+ acseTimeout: options.acseTimeout,
37310
+ dimseTimeout: options.dimseTimeout,
37311
+ noHostnameLookup: options.noHostnameLookup,
37312
+ verbosity: options.verbosity,
37313
+ required: params.required ?? options.required,
37314
+ timeoutMs,
37315
+ signal
37316
+ });
37317
+ if (!result.ok) return err(result.error);
37318
+ return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37319
+ };
37320
+ }
37321
+ function resolveConfig(options) {
37322
+ const mode = options.mode ?? "multiple";
37323
+ const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
37324
+ return {
37325
+ mode,
37326
+ configuredMaxAssociations: mode === "single" ? 1 : rawMax,
37327
+ maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
37328
+ defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
37329
+ defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
37330
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
37331
+ bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
37332
+ maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
37333
+ };
37334
+ }
37335
+ var DicomSender = class _DicomSender extends events.EventEmitter {
37336
+ constructor(engine, cfg, signal) {
37337
+ super();
37338
+ __publicField(this, "engine");
37339
+ __publicField(this, "defaultTimeoutMs");
37340
+ __publicField(this, "defaultMaxRetries");
37341
+ __publicField(this, "signal");
37342
+ __publicField(this, "abortHandler");
37343
+ this.setMaxListeners(20);
37344
+ this.on("error", () => {
37345
+ });
37346
+ this.engine = engine;
37347
+ this.defaultTimeoutMs = cfg.defaultTimeoutMs;
37348
+ this.defaultMaxRetries = cfg.defaultMaxRetries;
37349
+ this.signal = signal;
37350
+ if (signal !== void 0) {
37351
+ this.wireAbortSignal(signal);
37352
+ }
37353
+ }
37354
+ // -----------------------------------------------------------------------
37355
+ // Public API
37356
+ // -----------------------------------------------------------------------
37357
+ /**
37358
+ * Creates a new DicomSender instance.
37359
+ *
37360
+ * @param options - Configuration options
37361
+ * @returns A Result containing the instance or a validation error
37362
+ */
37363
+ static create(options) {
37364
+ const validation = DicomSenderOptionsSchema.safeParse(options);
37365
+ if (!validation.success) {
37366
+ return err(createValidationError("DicomSender", validation.error));
37367
+ }
37368
+ const cfg = resolveConfig(options);
37369
+ const senderRef = { current: void 0 };
37370
+ const engine = new SenderEngine({
37371
+ ...cfg,
37372
+ executor: createStorescuExecutor(options),
37373
+ signal: options.signal,
37374
+ senderName: "DicomSender",
37375
+ emitters: {
37376
+ emitSendComplete: (data) => {
37377
+ senderRef.current.emit("SEND_COMPLETE", data);
37378
+ },
37379
+ emitSendFailed: (data) => {
37380
+ senderRef.current.emit("SEND_FAILED", data);
37381
+ },
37382
+ emitHealthChanged: (data) => {
37383
+ senderRef.current.emit("HEALTH_CHANGED", data);
37384
+ },
37385
+ emitBucketFlushed: (data) => {
37386
+ senderRef.current.emit("BUCKET_FLUSHED", data);
37387
+ }
37388
+ }
37389
+ });
37390
+ const sender = new _DicomSender(engine, cfg, options.signal);
37391
+ senderRef.current = sender;
37392
+ return ok(sender);
37393
+ }
37394
+ /**
37395
+ * Sends one or more DICOM files to the remote endpoint.
37396
+ *
37397
+ * In single/multiple mode, files are sent as one storescu call.
37398
+ * In bucket mode, files are accumulated into a bucket and flushed
37399
+ * when the bucket reaches maxBucketSize or the flush timer fires.
37400
+ *
37401
+ * The returned promise resolves when the files are actually sent
37402
+ * (not just queued). Callers can await for confirmation or
37403
+ * fire-and-forget with `void sender.send(files)`.
37404
+ *
37405
+ * @param files - One or more DICOM file paths
37406
+ * @param options - Per-send overrides
37407
+ * @returns A Result containing the send result or an error
37408
+ */
37409
+ send(files, options) {
37410
+ const params = buildStorescuParams(options);
37411
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
37412
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
37413
+ return this.engine.send(files, timeoutMs, maxRetries, params);
37414
+ }
37415
+ /**
37416
+ * Flushes the current bucket immediately (bucket mode only).
37417
+ * In single/multiple mode this is a no-op.
37418
+ */
37419
+ flush() {
37420
+ this.engine.flush();
37421
+ }
37422
+ /**
37423
+ * Gracefully stops the sender. Rejects all queued items and
37424
+ * waits for active associations to complete.
37425
+ */
37426
+ async stop() {
37427
+ if (this.signal !== void 0 && this.abortHandler !== void 0) {
37428
+ this.signal.removeEventListener("abort", this.abortHandler);
37429
+ }
37430
+ await this.engine.stop();
37431
+ }
37432
+ /** Current sender status. */
37433
+ get status() {
37434
+ return this.engine.status;
37435
+ }
37436
+ // -----------------------------------------------------------------------
37437
+ // Typed event listener convenience methods
37438
+ // -----------------------------------------------------------------------
37439
+ /**
37440
+ * Registers a typed listener for a DicomSender-specific event.
37441
+ *
37442
+ * @param event - The event name from DicomSenderEventMap
37443
+ * @param listener - Callback receiving typed event data
37444
+ * @returns this for chaining
37445
+ */
37446
+ onEvent(event, listener) {
37447
+ return this.on(event, listener);
37448
+ }
37449
+ /**
37450
+ * Registers a listener for successful sends.
37451
+ *
37452
+ * @param listener - Callback receiving send complete data
37453
+ * @returns this for chaining
37454
+ */
37455
+ onSendComplete(listener) {
37456
+ return this.on("SEND_COMPLETE", listener);
37457
+ }
37458
+ /**
37459
+ * Registers a listener for failed sends.
37460
+ *
37461
+ * @param listener - Callback receiving send failed data
37462
+ * @returns this for chaining
37463
+ */
37464
+ onSendFailed(listener) {
37465
+ return this.on("SEND_FAILED", listener);
37466
+ }
37467
+ /**
37468
+ * Registers a listener for health state changes.
37469
+ *
37470
+ * @param listener - Callback receiving health change data
37471
+ * @returns this for chaining
37472
+ */
37473
+ onHealthChanged(listener) {
37474
+ return this.on("HEALTH_CHANGED", listener);
37475
+ }
37476
+ /**
37477
+ * Registers a listener for bucket flushes (bucket mode only).
37478
+ *
37479
+ * @param listener - Callback receiving bucket flush data
37480
+ * @returns this for chaining
37481
+ */
37482
+ onBucketFlushed(listener) {
37483
+ return this.on("BUCKET_FLUSHED", listener);
37484
+ }
37485
+ // -----------------------------------------------------------------------
37486
+ // Abort signal
37487
+ // -----------------------------------------------------------------------
37488
+ /** Wires an AbortSignal to stop the sender. */
37489
+ wireAbortSignal(signal) {
37490
+ if (signal.aborted) {
37491
+ void this.stop();
37492
+ return;
37493
+ }
37494
+ this.abortHandler = () => {
37495
+ void this.stop();
37496
+ };
37497
+ signal.addEventListener("abort", this.abortHandler, { once: true });
37498
+ }
37499
+ };
37500
+ function buildStorescuParams(options) {
37501
+ return {
37502
+ calledAETitle: options?.calledAETitle,
37503
+ callingAETitle: options?.callingAETitle,
37504
+ required: options?.required,
37505
+ proposedTransferSyntax: options?.proposedTransferSyntax,
37506
+ combineProposedTransferSyntaxes: options?.combineProposedTransferSyntaxes
37507
+ };
37508
+ }
37509
+ var DEFAULT_MAX_ASSOCIATIONS2 = 4;
37510
+ var DEFAULT_MAX_QUEUE_LENGTH2 = 1e3;
37511
+ var DEFAULT_MAX_RETRIES2 = 3;
37512
+ var DEFAULT_RETRY_DELAY_MS2 = 1e3;
37513
+ var DEFAULT_BUCKET_FLUSH_MS2 = 5e3;
37514
+ var DEFAULT_MAX_BUCKET_SIZE2 = 50;
37515
+ var MAX_ASSOCIATIONS_LIMIT2 = 64;
37516
+ var DicomSendOptionsSchema = zod.z.object({
37517
+ host: zod.z.string().min(1),
37518
+ port: zod.z.number().int().min(1).max(65535),
37519
+ calledAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37520
+ callingAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
37521
+ mode: zod.z.enum(["single", "multiple", "bucket"]).optional(),
37522
+ maxAssociations: zod.z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT2).optional(),
37523
+ maxQueueLength: zod.z.number().int().min(1).optional(),
37524
+ timeoutMs: zod.z.number().int().positive().optional(),
37525
+ maxRetries: zod.z.number().int().min(0).optional(),
37526
+ retryDelayMs: zod.z.number().int().min(0).optional(),
37527
+ bucketFlushMs: zod.z.number().int().positive().optional(),
37528
+ maxBucketSize: zod.z.number().int().min(1).optional(),
37529
+ maxPduReceive: zod.z.number().int().min(4096).max(131072).optional(),
37530
+ maxPduSend: zod.z.number().int().min(4096).max(131072).optional(),
37531
+ associationTimeout: zod.z.number().int().positive().optional(),
37532
+ acseTimeout: zod.z.number().int().positive().optional(),
37533
+ dimseTimeout: zod.z.number().int().positive().optional(),
37534
+ noHostnameLookup: zod.z.boolean().optional(),
37535
+ verbosity: zod.z.enum(["verbose", "debug"]).optional(),
37536
+ noHalt: zod.z.boolean().optional(),
37537
+ noIllegalProposal: zod.z.boolean().optional(),
37538
+ decompress: zod.z.enum(["never", "lossless", "lossy"]).optional(),
37539
+ multiAssociations: zod.z.boolean().optional(),
37540
+ noUidChecks: zod.z.boolean().optional(),
37541
+ signal: zod.z.instanceof(AbortSignal).optional()
37542
+ }).strict();
37543
+ function createDcmsendExecutor(options) {
37544
+ return async (files, timeoutMs, params, signal) => {
37545
+ const result = await dcmsend({
37546
+ host: options.host,
37547
+ port: options.port,
37548
+ files: [...files],
37549
+ calledAETitle: params.calledAETitle ?? options.calledAETitle,
37550
+ callingAETitle: params.callingAETitle ?? options.callingAETitle,
37551
+ maxPduReceive: options.maxPduReceive,
37552
+ maxPduSend: options.maxPduSend,
37553
+ associationTimeout: options.associationTimeout,
37554
+ acseTimeout: options.acseTimeout,
37555
+ dimseTimeout: options.dimseTimeout,
37556
+ noHostnameLookup: options.noHostnameLookup,
37557
+ verbosity: options.verbosity,
37558
+ noHalt: options.noHalt,
37559
+ noIllegalProposal: options.noIllegalProposal,
37560
+ decompress: options.decompress,
37561
+ multiAssociations: options.multiAssociations,
37562
+ noUidChecks: options.noUidChecks,
37563
+ timeoutMs,
37564
+ signal
37565
+ });
37566
+ if (!result.ok) return err(result.error);
37567
+ return ok({ stdout: result.value.stdout, stderr: result.value.stderr });
37568
+ };
37569
+ }
37570
+ function resolveConfig2(options) {
37571
+ const mode = options.mode ?? "multiple";
37572
+ const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS2;
37573
+ return {
37574
+ mode,
37575
+ configuredMaxAssociations: mode === "single" ? 1 : rawMax,
37576
+ maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH2,
37577
+ defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
37578
+ defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES2,
37579
+ retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS2,
37580
+ bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS2,
37581
+ maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE2
37582
+ };
37583
+ }
37584
+ var DicomSend = class _DicomSend extends events.EventEmitter {
37585
+ constructor(engine, cfg, signal) {
37586
+ super();
37587
+ __publicField(this, "engine");
37588
+ __publicField(this, "defaultTimeoutMs");
37589
+ __publicField(this, "defaultMaxRetries");
37590
+ __publicField(this, "signal");
37591
+ __publicField(this, "abortHandler");
37592
+ this.setMaxListeners(20);
37593
+ this.on("error", () => {
37594
+ });
37595
+ this.engine = engine;
37596
+ this.defaultTimeoutMs = cfg.defaultTimeoutMs;
37597
+ this.defaultMaxRetries = cfg.defaultMaxRetries;
37598
+ this.signal = signal;
37599
+ if (signal !== void 0) {
37600
+ this.wireAbortSignal(signal);
37601
+ }
37602
+ }
37603
+ // -----------------------------------------------------------------------
37604
+ // Public API
37605
+ // -----------------------------------------------------------------------
37606
+ /**
37607
+ * Creates a new DicomSend instance.
37608
+ *
37609
+ * @param options - Configuration options
37610
+ * @returns A Result containing the instance or a validation error
37611
+ */
37612
+ static create(options) {
37613
+ const validation = DicomSendOptionsSchema.safeParse(options);
37614
+ if (!validation.success) {
37615
+ return err(createValidationError("DicomSend", validation.error));
37616
+ }
37617
+ const cfg = resolveConfig2(options);
37618
+ const senderRef = { current: void 0 };
37619
+ const engine = new SenderEngine({
37620
+ ...cfg,
37621
+ executor: createDcmsendExecutor(options),
37622
+ signal: options.signal,
37623
+ senderName: "DicomSend",
37624
+ emitters: {
37625
+ emitSendComplete: (data) => {
37626
+ senderRef.current.emit("SEND_COMPLETE", data);
37627
+ },
37628
+ emitSendFailed: (data) => {
37629
+ senderRef.current.emit("SEND_FAILED", data);
37630
+ },
37631
+ emitHealthChanged: (data) => {
37632
+ senderRef.current.emit("HEALTH_CHANGED", data);
37633
+ },
37634
+ emitBucketFlushed: (data) => {
37635
+ senderRef.current.emit("BUCKET_FLUSHED", data);
37636
+ }
37637
+ }
37638
+ });
37639
+ const sender = new _DicomSend(engine, cfg, options.signal);
37640
+ senderRef.current = sender;
37641
+ return ok(sender);
37642
+ }
37643
+ /**
37644
+ * Sends one or more DICOM files to the remote endpoint.
37645
+ *
37646
+ * In single/multiple mode, files are sent as one dcmsend call.
37647
+ * In bucket mode, files are accumulated into a bucket and flushed
37648
+ * when the bucket reaches maxBucketSize or the flush timer fires.
37649
+ *
37650
+ * @param files - One or more DICOM file paths
37651
+ * @param options - Per-send overrides
37652
+ * @returns A Result containing the send result or an error
37653
+ */
37654
+ send(files, options) {
37655
+ const params = {
37656
+ calledAETitle: options?.calledAETitle,
37657
+ callingAETitle: options?.callingAETitle
37658
+ };
37659
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
37660
+ const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
37661
+ return this.engine.send(files, timeoutMs, maxRetries, params);
37662
+ }
37663
+ /**
37664
+ * Flushes the current bucket immediately (bucket mode only).
37665
+ * In single/multiple mode this is a no-op.
37666
+ */
37667
+ flush() {
37668
+ this.engine.flush();
37669
+ }
37670
+ /**
37671
+ * Gracefully stops the sender. Rejects all queued items and
37672
+ * waits for active associations to complete.
37673
+ */
37674
+ async stop() {
37675
+ if (this.signal !== void 0 && this.abortHandler !== void 0) {
37676
+ this.signal.removeEventListener("abort", this.abortHandler);
37677
+ }
37678
+ await this.engine.stop();
37679
+ }
37680
+ /** Current sender status. */
37681
+ get status() {
37682
+ return this.engine.status;
37683
+ }
37684
+ // -----------------------------------------------------------------------
37685
+ // Typed event listener convenience methods
37686
+ // -----------------------------------------------------------------------
37687
+ /**
37688
+ * Registers a typed listener for a DicomSend-specific event.
37689
+ *
37690
+ * @param event - The event name from DicomSendEventMap
37691
+ * @param listener - Callback receiving typed event data
37692
+ * @returns this for chaining
37693
+ */
37694
+ onEvent(event, listener) {
37695
+ return this.on(event, listener);
37696
+ }
37697
+ /**
37698
+ * Registers a listener for successful sends.
37699
+ *
37700
+ * @param listener - Callback receiving send complete data
37701
+ * @returns this for chaining
37702
+ */
37703
+ onSendComplete(listener) {
37704
+ return this.on("SEND_COMPLETE", listener);
37705
+ }
37706
+ /**
37707
+ * Registers a listener for failed sends.
37708
+ *
37709
+ * @param listener - Callback receiving send failed data
37710
+ * @returns this for chaining
37711
+ */
37712
+ onSendFailed(listener) {
37713
+ return this.on("SEND_FAILED", listener);
37714
+ }
37715
+ /**
37716
+ * Registers a listener for health state changes.
37717
+ *
37718
+ * @param listener - Callback receiving health change data
37719
+ * @returns this for chaining
37720
+ */
37721
+ onHealthChanged(listener) {
37722
+ return this.on("HEALTH_CHANGED", listener);
37723
+ }
37724
+ /**
37725
+ * Registers a listener for bucket flushes (bucket mode only).
37726
+ *
37727
+ * @param listener - Callback receiving bucket flush data
37728
+ * @returns this for chaining
37729
+ */
37730
+ onBucketFlushed(listener) {
37731
+ return this.on("BUCKET_FLUSHED", listener);
37732
+ }
37733
+ // -----------------------------------------------------------------------
37734
+ // Abort signal
37735
+ // -----------------------------------------------------------------------
37736
+ /** Wires an AbortSignal to stop the sender. */
37737
+ wireAbortSignal(signal) {
37738
+ if (signal.aborted) {
37739
+ void this.stop();
37740
+ return;
37741
+ }
37742
+ this.abortHandler = () => {
37743
+ void this.stop();
37744
+ };
37745
+ signal.addEventListener("abort", this.abortHandler, { once: true });
37746
+ }
37747
+ };
37748
+
37411
37749
  // src/pacs/types.ts
37412
37750
  var QueryLevel = {
37413
37751
  STUDY: "STUDY",
@@ -37957,7 +38295,7 @@ var DEFAULT_CONFIG = {
37957
38295
  signal: void 0,
37958
38296
  onRetry: void 0
37959
38297
  };
37960
- function resolveConfig2(opts) {
38298
+ function resolveConfig3(opts) {
37961
38299
  if (!opts) return DEFAULT_CONFIG;
37962
38300
  return {
37963
38301
  ...DEFAULT_CONFIG,
@@ -38002,7 +38340,7 @@ function shouldBreakAfterFailure(attempt, lastError, config) {
38002
38340
  return false;
38003
38341
  }
38004
38342
  async function retry(operation, options) {
38005
- const config = resolveConfig2(options);
38343
+ const config = resolveConfig3(options);
38006
38344
  let lastResult = err(new Error("No attempts executed"));
38007
38345
  for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
38008
38346
  lastResult = await operation();
@@ -38050,6 +38388,7 @@ exports.DcmtkProcess = DcmtkProcess;
38050
38388
  exports.DicomDataset = DicomDataset;
38051
38389
  exports.DicomInstance = DicomInstance;
38052
38390
  exports.DicomReceiver = DicomReceiver;
38391
+ exports.DicomSend = DicomSend;
38053
38392
  exports.DicomSender = DicomSender;
38054
38393
  exports.DicomTagPathSchema = DicomTagPathSchema;
38055
38394
  exports.DicomTagSchema = DicomTagSchema;