@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.cjs +596 -290
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -57
- package/dist/index.d.ts +198 -57
- package/dist/index.js +596 -291
- package/dist/index.js.map +1 -1
- package/dist/tools.cjs +38 -0
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +14 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.js +38 -0
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -36877,68 +36915,13 @@ var SenderHealth = {
|
|
|
36877
36915
|
DOWN: "down"
|
|
36878
36916
|
};
|
|
36879
36917
|
|
|
36880
|
-
// src/senders/
|
|
36881
|
-
var DEFAULT_MAX_ASSOCIATIONS = 4;
|
|
36882
|
-
var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
|
|
36883
|
-
var DEFAULT_MAX_RETRIES = 3;
|
|
36884
|
-
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
36885
|
-
var DEFAULT_BUCKET_FLUSH_MS = 5e3;
|
|
36886
|
-
var DEFAULT_MAX_BUCKET_SIZE = 50;
|
|
36887
|
-
var MAX_ASSOCIATIONS_LIMIT = 64;
|
|
36918
|
+
// src/senders/SenderEngine.ts
|
|
36888
36919
|
var DEGRADE_THRESHOLD = 3;
|
|
36889
36920
|
var DOWN_THRESHOLD = 10;
|
|
36890
36921
|
var RECOVERY_THRESHOLD = 3;
|
|
36891
|
-
var
|
|
36892
|
-
|
|
36893
|
-
|
|
36894
|
-
calledAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
36895
|
-
callingAETitle: zod.z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
36896
|
-
mode: zod.z.enum(["single", "multiple", "bucket"]).optional(),
|
|
36897
|
-
maxAssociations: zod.z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
|
|
36898
|
-
proposedTransferSyntax: zod.z.union([zod.z.enum(PROPOSED_TS_VALUES), zod.z.array(zod.z.enum(PROPOSED_TS_VALUES)).min(1)]).optional(),
|
|
36899
|
-
combineProposedTransferSyntaxes: zod.z.boolean().optional(),
|
|
36900
|
-
maxQueueLength: zod.z.number().int().min(1).optional(),
|
|
36901
|
-
timeoutMs: zod.z.number().int().positive().optional(),
|
|
36902
|
-
maxRetries: zod.z.number().int().min(0).optional(),
|
|
36903
|
-
retryDelayMs: zod.z.number().int().min(0).optional(),
|
|
36904
|
-
bucketFlushMs: zod.z.number().int().positive().optional(),
|
|
36905
|
-
maxBucketSize: zod.z.number().int().min(1).optional(),
|
|
36906
|
-
maxPduReceive: zod.z.number().int().min(4096).max(131072).optional(),
|
|
36907
|
-
maxPduSend: zod.z.number().int().min(4096).max(131072).optional(),
|
|
36908
|
-
associationTimeout: zod.z.number().int().positive().optional(),
|
|
36909
|
-
acseTimeout: zod.z.number().int().positive().optional(),
|
|
36910
|
-
dimseTimeout: zod.z.number().int().positive().optional(),
|
|
36911
|
-
noHostnameLookup: zod.z.boolean().optional(),
|
|
36912
|
-
verbosity: zod.z.enum(["verbose", "debug"]).optional(),
|
|
36913
|
-
required: zod.z.boolean().optional(),
|
|
36914
|
-
signal: zod.z.instanceof(AbortSignal).optional()
|
|
36915
|
-
}).strict();
|
|
36916
|
-
function resolveConfig(options) {
|
|
36917
|
-
const mode = options.mode ?? "multiple";
|
|
36918
|
-
const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
|
|
36919
|
-
return {
|
|
36920
|
-
mode,
|
|
36921
|
-
configuredMaxAssociations: mode === "single" ? 1 : rawMax,
|
|
36922
|
-
maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
|
|
36923
|
-
defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
36924
|
-
defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
36925
|
-
retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
|
|
36926
|
-
bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
|
|
36927
|
-
maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
|
|
36928
|
-
};
|
|
36929
|
-
}
|
|
36930
|
-
var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
36931
|
-
constructor(options) {
|
|
36932
|
-
super();
|
|
36933
|
-
__publicField(this, "options");
|
|
36934
|
-
__publicField(this, "mode");
|
|
36935
|
-
__publicField(this, "configuredMaxAssociations");
|
|
36936
|
-
__publicField(this, "maxQueueLength");
|
|
36937
|
-
__publicField(this, "defaultTimeoutMs");
|
|
36938
|
-
__publicField(this, "defaultMaxRetries");
|
|
36939
|
-
__publicField(this, "retryDelayMs");
|
|
36940
|
-
__publicField(this, "bucketFlushMs");
|
|
36941
|
-
__publicField(this, "maxBucketSize");
|
|
36922
|
+
var SenderEngine = class {
|
|
36923
|
+
constructor(config) {
|
|
36924
|
+
__publicField(this, "config");
|
|
36942
36925
|
// Queue and concurrency state
|
|
36943
36926
|
__publicField(this, "queue", []);
|
|
36944
36927
|
__publicField(this, "activeAssociations", 0);
|
|
@@ -36951,113 +36934,36 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
36951
36934
|
// Bucket state (bucket mode only)
|
|
36952
36935
|
__publicField(this, "currentBucket", []);
|
|
36953
36936
|
__publicField(this, "bucketTimer");
|
|
36954
|
-
|
|
36955
|
-
|
|
36956
|
-
this.setMaxListeners(20);
|
|
36957
|
-
this.on("error", () => {
|
|
36958
|
-
});
|
|
36959
|
-
this.options = options;
|
|
36960
|
-
const cfg = resolveConfig(options);
|
|
36961
|
-
this.mode = cfg.mode;
|
|
36962
|
-
this.configuredMaxAssociations = cfg.configuredMaxAssociations;
|
|
36963
|
-
this.effectiveMaxAssociations = cfg.configuredMaxAssociations;
|
|
36964
|
-
this.maxQueueLength = cfg.maxQueueLength;
|
|
36965
|
-
this.defaultTimeoutMs = cfg.defaultTimeoutMs;
|
|
36966
|
-
this.defaultMaxRetries = cfg.defaultMaxRetries;
|
|
36967
|
-
this.retryDelayMs = cfg.retryDelayMs;
|
|
36968
|
-
this.bucketFlushMs = cfg.bucketFlushMs;
|
|
36969
|
-
this.maxBucketSize = cfg.maxBucketSize;
|
|
36970
|
-
if (options.signal !== void 0) {
|
|
36971
|
-
this.wireAbortSignal(options.signal);
|
|
36972
|
-
}
|
|
36937
|
+
this.config = config;
|
|
36938
|
+
this.effectiveMaxAssociations = config.configuredMaxAssociations;
|
|
36973
36939
|
}
|
|
36974
36940
|
// -----------------------------------------------------------------------
|
|
36975
36941
|
// Public API
|
|
36976
36942
|
// -----------------------------------------------------------------------
|
|
36977
|
-
/**
|
|
36978
|
-
|
|
36979
|
-
*
|
|
36980
|
-
* @param options - Configuration options
|
|
36981
|
-
* @returns A Result containing the instance or a validation error
|
|
36982
|
-
*/
|
|
36983
|
-
static create(options) {
|
|
36984
|
-
const validation = DicomSenderOptionsSchema.safeParse(options);
|
|
36985
|
-
if (!validation.success) {
|
|
36986
|
-
return err(createValidationError("DicomSender", validation.error));
|
|
36987
|
-
}
|
|
36988
|
-
return ok(new _DicomSender(options));
|
|
36989
|
-
}
|
|
36990
|
-
/**
|
|
36991
|
-
* Sends one or more DICOM files to the remote endpoint.
|
|
36992
|
-
*
|
|
36993
|
-
* In single/multiple mode, files are sent as one storescu call.
|
|
36994
|
-
* In bucket mode, files are accumulated into a bucket and flushed
|
|
36995
|
-
* when the bucket reaches maxBucketSize or the flush timer fires.
|
|
36996
|
-
*
|
|
36997
|
-
* The returned promise resolves when the files are actually sent
|
|
36998
|
-
* (not just queued). Callers can await for confirmation or
|
|
36999
|
-
* fire-and-forget with `void sender.send(files)`.
|
|
37000
|
-
*
|
|
37001
|
-
* @param files - One or more DICOM file paths
|
|
37002
|
-
* @param options - Per-send overrides
|
|
37003
|
-
* @returns A Result containing the send result or an error
|
|
37004
|
-
*/
|
|
37005
|
-
send(files, options) {
|
|
36943
|
+
/** Sends files through the engine's queue/bucket system. */
|
|
36944
|
+
send(files, timeoutMs, maxRetries, binaryParams) {
|
|
37006
36945
|
if (this.isStopped) {
|
|
37007
|
-
return Promise.resolve(err(new Error(
|
|
36946
|
+
return Promise.resolve(err(new Error(`${this.config.senderName}: sender is stopped`)));
|
|
37008
36947
|
}
|
|
37009
36948
|
if (files.length === 0) {
|
|
37010
|
-
return Promise.resolve(err(new Error(
|
|
37011
|
-
}
|
|
37012
|
-
return this.dispatchSend(files, this.buildSendParams(options));
|
|
37013
|
-
}
|
|
37014
|
-
/** Builds SendParams from per-send options, applying instance defaults. */
|
|
37015
|
-
buildSendParams(options) {
|
|
37016
|
-
return {
|
|
37017
|
-
timeoutMs: options?.timeoutMs ?? this.defaultTimeoutMs,
|
|
37018
|
-
maxRetries: options?.maxRetries ?? this.defaultMaxRetries,
|
|
37019
|
-
calledAETitle: options?.calledAETitle,
|
|
37020
|
-
callingAETitle: options?.callingAETitle,
|
|
37021
|
-
required: options?.required,
|
|
37022
|
-
proposedTransferSyntax: options?.proposedTransferSyntax,
|
|
37023
|
-
combineProposedTransferSyntaxes: options?.combineProposedTransferSyntaxes
|
|
37024
|
-
};
|
|
37025
|
-
}
|
|
37026
|
-
/** Dispatches a send to the appropriate mode handler. */
|
|
37027
|
-
dispatchSend(files, params) {
|
|
37028
|
-
switch (this.mode) {
|
|
37029
|
-
case "single":
|
|
37030
|
-
case "multiple":
|
|
37031
|
-
return this.enqueueSend(files, params);
|
|
37032
|
-
case "bucket":
|
|
37033
|
-
return this.enqueueBucket(files, params);
|
|
37034
|
-
default:
|
|
37035
|
-
assertUnreachable(this.mode);
|
|
36949
|
+
return Promise.resolve(err(new Error(`${this.config.senderName}: no files provided`)));
|
|
37036
36950
|
}
|
|
36951
|
+
return this.dispatchSend(files, timeoutMs, maxRetries, binaryParams);
|
|
37037
36952
|
}
|
|
37038
|
-
/**
|
|
37039
|
-
* Flushes the current bucket immediately (bucket mode only).
|
|
37040
|
-
* In single/multiple mode this is a no-op.
|
|
37041
|
-
*/
|
|
36953
|
+
/** Flushes the current bucket immediately (bucket mode only). */
|
|
37042
36954
|
flush() {
|
|
37043
|
-
if (this.mode !== "bucket") return;
|
|
36955
|
+
if (this.config.mode !== "bucket") return;
|
|
37044
36956
|
if (this.currentBucket.length === 0) return;
|
|
37045
36957
|
this.clearBucketTimer();
|
|
37046
36958
|
this.flushBucketInternal("timer");
|
|
37047
36959
|
}
|
|
37048
|
-
/**
|
|
37049
|
-
* Gracefully stops the sender. Rejects all queued items and
|
|
37050
|
-
* waits for active associations to complete.
|
|
37051
|
-
*/
|
|
36960
|
+
/** Gracefully stops the engine. Rejects all queued items and waits for active associations. */
|
|
37052
36961
|
async stop() {
|
|
37053
36962
|
if (this.isStopped) return;
|
|
37054
36963
|
this.isStopped = true;
|
|
37055
|
-
if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
|
|
37056
|
-
this.options.signal.removeEventListener("abort", this.abortHandler);
|
|
37057
|
-
}
|
|
37058
36964
|
this.clearBucketTimer();
|
|
37059
|
-
this.rejectBucket(
|
|
37060
|
-
this.rejectQueue(
|
|
36965
|
+
this.rejectBucket(`${this.config.senderName}: sender stopped`);
|
|
36966
|
+
this.rejectQueue(`${this.config.senderName}: sender stopped`);
|
|
37061
36967
|
await this.waitForActive();
|
|
37062
36968
|
}
|
|
37063
36969
|
/** Current sender status. */
|
|
@@ -37072,77 +36978,37 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37072
36978
|
stopped: this.isStopped
|
|
37073
36979
|
};
|
|
37074
36980
|
}
|
|
36981
|
+
/** Whether the engine has been stopped. */
|
|
36982
|
+
get stopped() {
|
|
36983
|
+
return this.isStopped;
|
|
36984
|
+
}
|
|
37075
36985
|
// -----------------------------------------------------------------------
|
|
37076
|
-
//
|
|
36986
|
+
// Dispatch
|
|
37077
36987
|
// -----------------------------------------------------------------------
|
|
37078
|
-
/**
|
|
37079
|
-
|
|
37080
|
-
|
|
37081
|
-
|
|
37082
|
-
|
|
37083
|
-
|
|
37084
|
-
|
|
37085
|
-
|
|
37086
|
-
|
|
37087
|
-
|
|
37088
|
-
|
|
37089
|
-
* Registers a listener for successful sends.
|
|
37090
|
-
*
|
|
37091
|
-
* @param listener - Callback receiving send complete data
|
|
37092
|
-
* @returns this for chaining
|
|
37093
|
-
*/
|
|
37094
|
-
onSendComplete(listener) {
|
|
37095
|
-
return this.on("SEND_COMPLETE", listener);
|
|
37096
|
-
}
|
|
37097
|
-
/**
|
|
37098
|
-
* Registers a listener for failed sends.
|
|
37099
|
-
*
|
|
37100
|
-
* @param listener - Callback receiving send failed data
|
|
37101
|
-
* @returns this for chaining
|
|
37102
|
-
*/
|
|
37103
|
-
onSendFailed(listener) {
|
|
37104
|
-
return this.on("SEND_FAILED", listener);
|
|
37105
|
-
}
|
|
37106
|
-
/**
|
|
37107
|
-
* Registers a listener for health state changes.
|
|
37108
|
-
*
|
|
37109
|
-
* @param listener - Callback receiving health change data
|
|
37110
|
-
* @returns this for chaining
|
|
37111
|
-
*/
|
|
37112
|
-
onHealthChanged(listener) {
|
|
37113
|
-
return this.on("HEALTH_CHANGED", listener);
|
|
37114
|
-
}
|
|
37115
|
-
/**
|
|
37116
|
-
* Registers a listener for bucket flushes (bucket mode only).
|
|
37117
|
-
*
|
|
37118
|
-
* @param listener - Callback receiving bucket flush data
|
|
37119
|
-
* @returns this for chaining
|
|
37120
|
-
*/
|
|
37121
|
-
onBucketFlushed(listener) {
|
|
37122
|
-
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
|
+
}
|
|
37123
36999
|
}
|
|
37124
37000
|
// -----------------------------------------------------------------------
|
|
37125
37001
|
// Single/Multiple mode: queue-based dispatch
|
|
37126
37002
|
// -----------------------------------------------------------------------
|
|
37127
37003
|
/** Enqueues a send and dispatches immediately if capacity allows. */
|
|
37128
|
-
enqueueSend(files,
|
|
37004
|
+
enqueueSend(files, timeoutMs, maxRetries, binaryParams) {
|
|
37129
37005
|
return new Promise((resolve) => {
|
|
37130
37006
|
const totalQueued = this.queue.length + this.currentBucket.length;
|
|
37131
|
-
if (totalQueued >= this.maxQueueLength) {
|
|
37132
|
-
resolve(err(new Error(
|
|
37007
|
+
if (totalQueued >= this.config.maxQueueLength) {
|
|
37008
|
+
resolve(err(new Error(`${this.config.senderName}: queue full`)));
|
|
37133
37009
|
return;
|
|
37134
37010
|
}
|
|
37135
|
-
const entry = {
|
|
37136
|
-
files,
|
|
37137
|
-
timeoutMs: params.timeoutMs,
|
|
37138
|
-
maxRetries: params.maxRetries,
|
|
37139
|
-
calledAETitle: params.calledAETitle,
|
|
37140
|
-
callingAETitle: params.callingAETitle,
|
|
37141
|
-
required: params.required,
|
|
37142
|
-
proposedTransferSyntax: params.proposedTransferSyntax,
|
|
37143
|
-
combineProposedTransferSyntaxes: params.combineProposedTransferSyntaxes,
|
|
37144
|
-
resolve
|
|
37145
|
-
};
|
|
37011
|
+
const entry = { files, timeoutMs, maxRetries, binaryParams, resolve };
|
|
37146
37012
|
if (this.activeAssociations < this.effectiveMaxAssociations) {
|
|
37147
37013
|
void this.executeEntry(entry);
|
|
37148
37014
|
} else {
|
|
@@ -37162,27 +37028,16 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37162
37028
|
// Bucket mode: accumulate-then-flush
|
|
37163
37029
|
// -----------------------------------------------------------------------
|
|
37164
37030
|
/** Adds files to the current bucket and triggers flush if full. */
|
|
37165
|
-
enqueueBucket(files,
|
|
37031
|
+
enqueueBucket(files, timeoutMs, maxRetries, binaryParams) {
|
|
37166
37032
|
return new Promise((resolve) => {
|
|
37167
37033
|
const totalQueued = this.queue.length + this.currentBucket.length;
|
|
37168
|
-
if (totalQueued >= this.maxQueueLength) {
|
|
37169
|
-
resolve(err(new Error(
|
|
37034
|
+
if (totalQueued >= this.config.maxQueueLength) {
|
|
37035
|
+
resolve(err(new Error(`${this.config.senderName}: queue full`)));
|
|
37170
37036
|
return;
|
|
37171
37037
|
}
|
|
37172
|
-
|
|
37173
|
-
this.currentBucket.push({
|
|
37174
|
-
files,
|
|
37175
|
-
resolve,
|
|
37176
|
-
timeoutMs,
|
|
37177
|
-
maxRetries,
|
|
37178
|
-
calledAETitle,
|
|
37179
|
-
callingAETitle,
|
|
37180
|
-
required,
|
|
37181
|
-
proposedTransferSyntax,
|
|
37182
|
-
combineProposedTransferSyntaxes
|
|
37183
|
-
});
|
|
37038
|
+
this.currentBucket.push({ files, resolve, timeoutMs, maxRetries, binaryParams });
|
|
37184
37039
|
const totalFiles = this.countBucketFiles();
|
|
37185
|
-
if (totalFiles >= this.maxBucketSize) {
|
|
37040
|
+
if (totalFiles >= this.config.maxBucketSize) {
|
|
37186
37041
|
this.clearBucketTimer();
|
|
37187
37042
|
void this.flushBucketInternal("maxSize");
|
|
37188
37043
|
} else {
|
|
@@ -37214,11 +37069,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37214
37069
|
files: allFiles,
|
|
37215
37070
|
timeoutMs,
|
|
37216
37071
|
maxRetries,
|
|
37217
|
-
|
|
37218
|
-
callingAETitle: entries[0]?.callingAETitle,
|
|
37219
|
-
required: entries[0]?.required,
|
|
37220
|
-
proposedTransferSyntax: entries[0]?.proposedTransferSyntax,
|
|
37221
|
-
combineProposedTransferSyntaxes: entries[0]?.combineProposedTransferSyntaxes,
|
|
37072
|
+
binaryParams: entries[0].binaryParams,
|
|
37222
37073
|
resolve: (result) => {
|
|
37223
37074
|
for (let i = 0; i < entries.length; i++) {
|
|
37224
37075
|
entries[i].resolve(result);
|
|
@@ -37232,7 +37083,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37232
37083
|
const entries = [...this.currentBucket];
|
|
37233
37084
|
this.currentBucket = [];
|
|
37234
37085
|
const bucketEntry = this.mergeBucketEntries(entries);
|
|
37235
|
-
this.
|
|
37086
|
+
this.config.emitters.emitBucketFlushed({ fileCount: bucketEntry.files.length, reason });
|
|
37236
37087
|
if (this.activeAssociations < this.effectiveMaxAssociations) {
|
|
37237
37088
|
void this.executeEntry(bucketEntry);
|
|
37238
37089
|
} else {
|
|
@@ -37245,7 +37096,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37245
37096
|
this.bucketTimer = setTimeout(() => {
|
|
37246
37097
|
this.bucketTimer = void 0;
|
|
37247
37098
|
void this.flushBucketInternal("timer");
|
|
37248
|
-
}, this.bucketFlushMs);
|
|
37099
|
+
}, this.config.bucketFlushMs);
|
|
37249
37100
|
}
|
|
37250
37101
|
/** Clears the bucket flush timer. */
|
|
37251
37102
|
clearBucketTimer() {
|
|
@@ -37257,7 +37108,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37257
37108
|
// -----------------------------------------------------------------------
|
|
37258
37109
|
// Core send execution with retry
|
|
37259
37110
|
// -----------------------------------------------------------------------
|
|
37260
|
-
/** Executes a single queue entry: calls
|
|
37111
|
+
/** Executes a single queue entry: calls the binary with retry. */
|
|
37261
37112
|
async executeEntry(entry) {
|
|
37262
37113
|
this.activeAssociations++;
|
|
37263
37114
|
const startMs = Date.now();
|
|
@@ -37266,7 +37117,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37266
37117
|
if (failure === void 0) return;
|
|
37267
37118
|
this.activeAssociations--;
|
|
37268
37119
|
this.recordFailure();
|
|
37269
|
-
this.
|
|
37120
|
+
this.config.emitters.emitSendFailed({
|
|
37270
37121
|
files: entry.files,
|
|
37271
37122
|
error: failure.error,
|
|
37272
37123
|
attempts: maxAttempts,
|
|
@@ -37276,49 +37127,27 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37276
37127
|
entry.resolve(err(failure.error));
|
|
37277
37128
|
this.drainQueue();
|
|
37278
37129
|
}
|
|
37279
|
-
/** Attempts
|
|
37130
|
+
/** Attempts the binary call up to maxAttempts times. Returns undefined on success. */
|
|
37280
37131
|
async attemptSend(entry, maxAttempts, startMs) {
|
|
37281
37132
|
let lastError;
|
|
37282
37133
|
const lastOutput = { stdout: "", stderr: "" };
|
|
37283
37134
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
37284
37135
|
if (this.isStopped) {
|
|
37285
37136
|
this.activeAssociations--;
|
|
37286
|
-
entry.resolve(err(new Error(
|
|
37137
|
+
entry.resolve(err(new Error(`${this.config.senderName}: sender stopped`)));
|
|
37287
37138
|
return void 0;
|
|
37288
37139
|
}
|
|
37289
|
-
const result = await this.
|
|
37140
|
+
const result = await this.config.executor(entry.files, entry.timeoutMs, entry.binaryParams, this.config.signal);
|
|
37290
37141
|
if (result.ok) {
|
|
37291
37142
|
this.handleSendSuccess(entry, startMs, result.value);
|
|
37292
37143
|
return void 0;
|
|
37293
37144
|
}
|
|
37294
37145
|
lastError = result.error;
|
|
37295
37146
|
if (attempt < maxAttempts - 1) {
|
|
37296
|
-
await delay2(this.retryDelayMs * (attempt + 1));
|
|
37147
|
+
await delay2(this.config.retryDelayMs * (attempt + 1));
|
|
37297
37148
|
}
|
|
37298
37149
|
}
|
|
37299
|
-
return { error: lastError ?? new Error(
|
|
37300
|
-
}
|
|
37301
|
-
/** Calls storescu with the configured options. */
|
|
37302
|
-
callStorescu(entry) {
|
|
37303
|
-
return storescu({
|
|
37304
|
-
host: this.options.host,
|
|
37305
|
-
port: this.options.port,
|
|
37306
|
-
files: [...entry.files],
|
|
37307
|
-
calledAETitle: entry.calledAETitle ?? this.options.calledAETitle,
|
|
37308
|
-
callingAETitle: entry.callingAETitle ?? this.options.callingAETitle,
|
|
37309
|
-
proposedTransferSyntax: entry.proposedTransferSyntax ?? this.options.proposedTransferSyntax,
|
|
37310
|
-
combineProposedTransferSyntaxes: entry.combineProposedTransferSyntaxes ?? this.options.combineProposedTransferSyntaxes,
|
|
37311
|
-
maxPduReceive: this.options.maxPduReceive,
|
|
37312
|
-
maxPduSend: this.options.maxPduSend,
|
|
37313
|
-
associationTimeout: this.options.associationTimeout,
|
|
37314
|
-
acseTimeout: this.options.acseTimeout,
|
|
37315
|
-
dimseTimeout: this.options.dimseTimeout,
|
|
37316
|
-
noHostnameLookup: this.options.noHostnameLookup,
|
|
37317
|
-
verbosity: this.options.verbosity,
|
|
37318
|
-
required: entry.required ?? this.options.required,
|
|
37319
|
-
timeoutMs: entry.timeoutMs,
|
|
37320
|
-
signal: this.options.signal
|
|
37321
|
-
});
|
|
37150
|
+
return { error: lastError ?? new Error(`${this.config.senderName}: send failed`), output: lastOutput };
|
|
37322
37151
|
}
|
|
37323
37152
|
/** Handles a successful send: updates state, emits event, resolves promise. */
|
|
37324
37153
|
handleSendSuccess(entry, startMs, output) {
|
|
@@ -37326,7 +37155,7 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37326
37155
|
const durationMs = Date.now() - startMs;
|
|
37327
37156
|
this.recordSuccess();
|
|
37328
37157
|
const data = { files: entry.files, fileCount: entry.files.length, durationMs, stdout: output.stdout, stderr: output.stderr };
|
|
37329
|
-
this.
|
|
37158
|
+
this.config.emitters.emitSendComplete(data);
|
|
37330
37159
|
entry.resolve(ok(data));
|
|
37331
37160
|
this.drainQueue();
|
|
37332
37161
|
}
|
|
@@ -37344,8 +37173,8 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37344
37173
|
if (this.health === SenderHealth.DOWN) {
|
|
37345
37174
|
this.health = SenderHealth.DEGRADED;
|
|
37346
37175
|
} else {
|
|
37347
|
-
this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.configuredMaxAssociations);
|
|
37348
|
-
if (this.effectiveMaxAssociations >= this.configuredMaxAssociations) {
|
|
37176
|
+
this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.config.configuredMaxAssociations);
|
|
37177
|
+
if (this.effectiveMaxAssociations >= this.config.configuredMaxAssociations) {
|
|
37349
37178
|
this.health = SenderHealth.HEALTHY;
|
|
37350
37179
|
}
|
|
37351
37180
|
}
|
|
@@ -37365,22 +37194,26 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37365
37194
|
this.emitHealthChanged(previousHealth);
|
|
37366
37195
|
}
|
|
37367
37196
|
} else if (this.consecutiveFailures >= DEGRADE_THRESHOLD && this.consecutiveFailures % DEGRADE_THRESHOLD === 0) {
|
|
37368
|
-
|
|
37369
|
-
|
|
37370
|
-
|
|
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;
|
|
37371
37210
|
this.emitHealthChanged(previousHealth);
|
|
37372
|
-
} else if (this.health === SenderHealth.DEGRADED) {
|
|
37373
|
-
const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
|
|
37374
|
-
if (newMax !== this.effectiveMaxAssociations) {
|
|
37375
|
-
this.effectiveMaxAssociations = newMax;
|
|
37376
|
-
this.emitHealthChanged(previousHealth);
|
|
37377
|
-
}
|
|
37378
37211
|
}
|
|
37379
37212
|
}
|
|
37380
37213
|
}
|
|
37381
|
-
/** Emits a HEALTH_CHANGED event. */
|
|
37214
|
+
/** Emits a HEALTH_CHANGED event via the wrapper's emitter. */
|
|
37382
37215
|
emitHealthChanged(previousHealth) {
|
|
37383
|
-
this.
|
|
37216
|
+
this.config.emitters.emitHealthChanged({
|
|
37384
37217
|
previousHealth,
|
|
37385
37218
|
newHealth: this.health,
|
|
37386
37219
|
effectiveMaxAssociations: this.effectiveMaxAssociations,
|
|
@@ -37420,20 +37253,6 @@ var DicomSender = class _DicomSender extends events.EventEmitter {
|
|
|
37420
37253
|
check();
|
|
37421
37254
|
});
|
|
37422
37255
|
}
|
|
37423
|
-
// -----------------------------------------------------------------------
|
|
37424
|
-
// Abort signal
|
|
37425
|
-
// -----------------------------------------------------------------------
|
|
37426
|
-
/** Wires an AbortSignal to stop the sender. */
|
|
37427
|
-
wireAbortSignal(signal) {
|
|
37428
|
-
if (signal.aborted) {
|
|
37429
|
-
void this.stop();
|
|
37430
|
-
return;
|
|
37431
|
-
}
|
|
37432
|
-
this.abortHandler = () => {
|
|
37433
|
-
void this.stop();
|
|
37434
|
-
};
|
|
37435
|
-
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
37436
|
-
}
|
|
37437
37256
|
};
|
|
37438
37257
|
function delay2(ms) {
|
|
37439
37258
|
return new Promise((resolve) => {
|
|
@@ -37441,6 +37260,492 @@ function delay2(ms) {
|
|
|
37441
37260
|
});
|
|
37442
37261
|
}
|
|
37443
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
|
+
|
|
37444
37749
|
// src/pacs/types.ts
|
|
37445
37750
|
var QueryLevel = {
|
|
37446
37751
|
STUDY: "STUDY",
|
|
@@ -37990,7 +38295,7 @@ var DEFAULT_CONFIG = {
|
|
|
37990
38295
|
signal: void 0,
|
|
37991
38296
|
onRetry: void 0
|
|
37992
38297
|
};
|
|
37993
|
-
function
|
|
38298
|
+
function resolveConfig3(opts) {
|
|
37994
38299
|
if (!opts) return DEFAULT_CONFIG;
|
|
37995
38300
|
return {
|
|
37996
38301
|
...DEFAULT_CONFIG,
|
|
@@ -38035,7 +38340,7 @@ function shouldBreakAfterFailure(attempt, lastError, config) {
|
|
|
38035
38340
|
return false;
|
|
38036
38341
|
}
|
|
38037
38342
|
async function retry(operation, options) {
|
|
38038
|
-
const config =
|
|
38343
|
+
const config = resolveConfig3(options);
|
|
38039
38344
|
let lastResult = err(new Error("No attempts executed"));
|
|
38040
38345
|
for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
|
|
38041
38346
|
lastResult = await operation();
|
|
@@ -38083,6 +38388,7 @@ exports.DcmtkProcess = DcmtkProcess;
|
|
|
38083
38388
|
exports.DicomDataset = DicomDataset;
|
|
38084
38389
|
exports.DicomInstance = DicomInstance;
|
|
38085
38390
|
exports.DicomReceiver = DicomReceiver;
|
|
38391
|
+
exports.DicomSend = DicomSend;
|
|
38086
38392
|
exports.DicomSender = DicomSender;
|
|
38087
38393
|
exports.DicomTagPathSchema = DicomTagPathSchema;
|
|
38088
38394
|
exports.DicomTagSchema = DicomTagSchema;
|