@ubercode/dcmtk 0.2.0 → 0.3.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/README.md +13 -11
- package/dist/{DicomInstance-CQEIuF_x.d.ts → DicomInstance-D9plqHp5.d.ts} +1 -1
- package/dist/{DicomInstance-By9zd7GM.d.cts → DicomInstance-DNHPkkzl.d.cts} +1 -1
- package/dist/dicom.cjs.map +1 -1
- package/dist/dicom.d.cts +2 -2
- package/dist/dicom.d.ts +2 -2
- package/dist/dicom.js.map +1 -1
- package/dist/index.cjs +533 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +319 -6
- package/dist/index.d.ts +319 -6
- package/dist/index.js +531 -5
- package/dist/index.js.map +1 -1
- package/dist/servers.cjs +1 -1
- package/dist/servers.cjs.map +1 -1
- package/dist/servers.d.cts +1 -1
- package/dist/servers.d.ts +1 -1
- package/dist/servers.js +1 -1
- package/dist/servers.js.map +1 -1
- package/dist/tools.cjs +1 -1
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ function mapResult(result, fn) {
|
|
|
36
36
|
|
|
37
37
|
// src/patterns.ts
|
|
38
38
|
var DICOM_TAG_PATTERN = /^\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)$/;
|
|
39
|
-
var AE_TITLE_PATTERN = /^[
|
|
39
|
+
var AE_TITLE_PATTERN = /^[\x20-\x5b\x5d-\x7e]+$/;
|
|
40
40
|
var UID_PATTERN = /^[0-9]+(\.[0-9]+)*$/;
|
|
41
41
|
var TAG_PATH_SEGMENT = /\([0-9A-Fa-f]{4},[0-9A-Fa-f]{4}\)(\[\d+\])?/;
|
|
42
42
|
var DICOM_TAG_PATH_PATTERN = new RegExp(`^${TAG_PATH_SEGMENT.source}(\\.${TAG_PATH_SEGMENT.source})*$`);
|
|
@@ -69,7 +69,7 @@ function createAETitle(input) {
|
|
|
69
69
|
return err(new Error(`Invalid AE Title: "${input}". Must be ${AE_TITLE_MIN_LENGTH}-${AE_TITLE_MAX_LENGTH} characters`));
|
|
70
70
|
}
|
|
71
71
|
if (!AE_TITLE_PATTERN.test(input)) {
|
|
72
|
-
return err(new Error(`Invalid AE Title: "${input}". Only
|
|
72
|
+
return err(new Error(`Invalid AE Title: "${input}". Only printable ASCII characters (no backslash) are allowed`));
|
|
73
73
|
}
|
|
74
74
|
return ok(input);
|
|
75
75
|
}
|
|
@@ -35992,6 +35992,532 @@ function delay(ms) {
|
|
|
35992
35992
|
});
|
|
35993
35993
|
}
|
|
35994
35994
|
|
|
35995
|
+
// src/senders/types.ts
|
|
35996
|
+
var SenderMode = {
|
|
35997
|
+
/** One association at a time, queued FIFO. */
|
|
35998
|
+
SINGLE: "single",
|
|
35999
|
+
/** Up to N concurrent associations, each send() gets its own. */
|
|
36000
|
+
MULTIPLE: "multiple",
|
|
36001
|
+
/** Files accumulated into buckets, each bucket = one association. */
|
|
36002
|
+
BUCKET: "bucket"
|
|
36003
|
+
};
|
|
36004
|
+
var SenderHealth = {
|
|
36005
|
+
/** All associations succeeding normally. */
|
|
36006
|
+
HEALTHY: "healthy",
|
|
36007
|
+
/** Recent failures detected; effective concurrency reduced. */
|
|
36008
|
+
DEGRADED: "degraded",
|
|
36009
|
+
/** Remote endpoint appears down; minimal concurrency. */
|
|
36010
|
+
DOWN: "down"
|
|
36011
|
+
};
|
|
36012
|
+
|
|
36013
|
+
// src/senders/DicomSender.ts
|
|
36014
|
+
var DEFAULT_MAX_ASSOCIATIONS = 4;
|
|
36015
|
+
var DEFAULT_MAX_QUEUE_LENGTH = 1e3;
|
|
36016
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
36017
|
+
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
36018
|
+
var DEFAULT_BUCKET_FLUSH_MS = 5e3;
|
|
36019
|
+
var DEFAULT_MAX_BUCKET_SIZE = 50;
|
|
36020
|
+
var MAX_ASSOCIATIONS_LIMIT = 64;
|
|
36021
|
+
var DEGRADE_THRESHOLD = 3;
|
|
36022
|
+
var DOWN_THRESHOLD = 10;
|
|
36023
|
+
var RECOVERY_THRESHOLD = 3;
|
|
36024
|
+
var DicomSenderOptionsSchema = z.object({
|
|
36025
|
+
host: z.string().min(1),
|
|
36026
|
+
port: z.number().int().min(1).max(65535),
|
|
36027
|
+
calledAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
36028
|
+
callingAETitle: z.string().min(1).max(16).refine(isValidAETitle, { message: "AE Title contains invalid characters" }).optional(),
|
|
36029
|
+
mode: z.enum(["single", "multiple", "bucket"]).optional(),
|
|
36030
|
+
maxAssociations: z.number().int().min(1).max(MAX_ASSOCIATIONS_LIMIT).optional(),
|
|
36031
|
+
proposedTransferSyntax: z.enum([
|
|
36032
|
+
"uncompressed",
|
|
36033
|
+
"littleEndian",
|
|
36034
|
+
"bigEndian",
|
|
36035
|
+
"implicitVR",
|
|
36036
|
+
"jpegLossless",
|
|
36037
|
+
"jpeg8Bit",
|
|
36038
|
+
"jpeg12Bit",
|
|
36039
|
+
"j2kLossless",
|
|
36040
|
+
"j2kLossy",
|
|
36041
|
+
"jlsLossless",
|
|
36042
|
+
"jlsLossy"
|
|
36043
|
+
]).optional(),
|
|
36044
|
+
maxQueueLength: z.number().int().min(1).optional(),
|
|
36045
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
36046
|
+
maxRetries: z.number().int().min(0).optional(),
|
|
36047
|
+
retryDelayMs: z.number().int().min(0).optional(),
|
|
36048
|
+
bucketFlushMs: z.number().int().positive().optional(),
|
|
36049
|
+
maxBucketSize: z.number().int().min(1).optional(),
|
|
36050
|
+
signal: z.instanceof(AbortSignal).optional()
|
|
36051
|
+
}).strict();
|
|
36052
|
+
function resolveConfig(options) {
|
|
36053
|
+
const mode = options.mode ?? "multiple";
|
|
36054
|
+
const rawMax = options.maxAssociations ?? DEFAULT_MAX_ASSOCIATIONS;
|
|
36055
|
+
return {
|
|
36056
|
+
mode,
|
|
36057
|
+
configuredMaxAssociations: mode === "single" ? 1 : rawMax,
|
|
36058
|
+
maxQueueLength: options.maxQueueLength ?? DEFAULT_MAX_QUEUE_LENGTH,
|
|
36059
|
+
defaultTimeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
36060
|
+
defaultMaxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
36061
|
+
retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
|
|
36062
|
+
bucketFlushMs: options.bucketFlushMs ?? DEFAULT_BUCKET_FLUSH_MS,
|
|
36063
|
+
maxBucketSize: options.maxBucketSize ?? DEFAULT_MAX_BUCKET_SIZE
|
|
36064
|
+
};
|
|
36065
|
+
}
|
|
36066
|
+
var DicomSender = class _DicomSender extends EventEmitter {
|
|
36067
|
+
constructor(options) {
|
|
36068
|
+
super();
|
|
36069
|
+
__publicField(this, "options");
|
|
36070
|
+
__publicField(this, "mode");
|
|
36071
|
+
__publicField(this, "configuredMaxAssociations");
|
|
36072
|
+
__publicField(this, "maxQueueLength");
|
|
36073
|
+
__publicField(this, "defaultTimeoutMs");
|
|
36074
|
+
__publicField(this, "defaultMaxRetries");
|
|
36075
|
+
__publicField(this, "retryDelayMs");
|
|
36076
|
+
__publicField(this, "bucketFlushMs");
|
|
36077
|
+
__publicField(this, "maxBucketSize");
|
|
36078
|
+
// Queue and concurrency state
|
|
36079
|
+
__publicField(this, "queue", []);
|
|
36080
|
+
__publicField(this, "activeAssociations", 0);
|
|
36081
|
+
__publicField(this, "isStopped", false);
|
|
36082
|
+
// Backpressure state
|
|
36083
|
+
__publicField(this, "health", SenderHealth.HEALTHY);
|
|
36084
|
+
__publicField(this, "effectiveMaxAssociations");
|
|
36085
|
+
__publicField(this, "consecutiveFailures", 0);
|
|
36086
|
+
__publicField(this, "consecutiveSuccesses", 0);
|
|
36087
|
+
// Bucket state (bucket mode only)
|
|
36088
|
+
__publicField(this, "currentBucket", []);
|
|
36089
|
+
__publicField(this, "bucketTimer");
|
|
36090
|
+
// AbortSignal
|
|
36091
|
+
__publicField(this, "abortHandler");
|
|
36092
|
+
this.setMaxListeners(20);
|
|
36093
|
+
this.on("error", () => {
|
|
36094
|
+
});
|
|
36095
|
+
this.options = options;
|
|
36096
|
+
const cfg = resolveConfig(options);
|
|
36097
|
+
this.mode = cfg.mode;
|
|
36098
|
+
this.configuredMaxAssociations = cfg.configuredMaxAssociations;
|
|
36099
|
+
this.effectiveMaxAssociations = cfg.configuredMaxAssociations;
|
|
36100
|
+
this.maxQueueLength = cfg.maxQueueLength;
|
|
36101
|
+
this.defaultTimeoutMs = cfg.defaultTimeoutMs;
|
|
36102
|
+
this.defaultMaxRetries = cfg.defaultMaxRetries;
|
|
36103
|
+
this.retryDelayMs = cfg.retryDelayMs;
|
|
36104
|
+
this.bucketFlushMs = cfg.bucketFlushMs;
|
|
36105
|
+
this.maxBucketSize = cfg.maxBucketSize;
|
|
36106
|
+
if (options.signal !== void 0) {
|
|
36107
|
+
this.wireAbortSignal(options.signal);
|
|
36108
|
+
}
|
|
36109
|
+
}
|
|
36110
|
+
// -----------------------------------------------------------------------
|
|
36111
|
+
// Public API
|
|
36112
|
+
// -----------------------------------------------------------------------
|
|
36113
|
+
/**
|
|
36114
|
+
* Creates a new DicomSender instance.
|
|
36115
|
+
*
|
|
36116
|
+
* @param options - Configuration options
|
|
36117
|
+
* @returns A Result containing the instance or a validation error
|
|
36118
|
+
*/
|
|
36119
|
+
static create(options) {
|
|
36120
|
+
const validation = DicomSenderOptionsSchema.safeParse(options);
|
|
36121
|
+
if (!validation.success) {
|
|
36122
|
+
return err(createValidationError("DicomSender", validation.error));
|
|
36123
|
+
}
|
|
36124
|
+
return ok(new _DicomSender(options));
|
|
36125
|
+
}
|
|
36126
|
+
/**
|
|
36127
|
+
* Sends one or more DICOM files to the remote endpoint.
|
|
36128
|
+
*
|
|
36129
|
+
* In single/multiple mode, files are sent as one storescu call.
|
|
36130
|
+
* In bucket mode, files are accumulated into a bucket and flushed
|
|
36131
|
+
* when the bucket reaches maxBucketSize or the flush timer fires.
|
|
36132
|
+
*
|
|
36133
|
+
* The returned promise resolves when the files are actually sent
|
|
36134
|
+
* (not just queued). Callers can await for confirmation or
|
|
36135
|
+
* fire-and-forget with `void sender.send(files)`.
|
|
36136
|
+
*
|
|
36137
|
+
* @param files - One or more DICOM file paths
|
|
36138
|
+
* @param options - Per-send overrides
|
|
36139
|
+
* @returns A Result containing the send result or an error
|
|
36140
|
+
*/
|
|
36141
|
+
send(files, options) {
|
|
36142
|
+
if (this.isStopped) {
|
|
36143
|
+
return Promise.resolve(err(new Error("DicomSender: sender is stopped")));
|
|
36144
|
+
}
|
|
36145
|
+
if (files.length === 0) {
|
|
36146
|
+
return Promise.resolve(err(new Error("DicomSender: no files provided")));
|
|
36147
|
+
}
|
|
36148
|
+
const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
|
|
36149
|
+
const maxRetries = options?.maxRetries ?? this.defaultMaxRetries;
|
|
36150
|
+
switch (this.mode) {
|
|
36151
|
+
case "single":
|
|
36152
|
+
case "multiple":
|
|
36153
|
+
return this.enqueueSend(files, timeoutMs, maxRetries);
|
|
36154
|
+
case "bucket":
|
|
36155
|
+
return this.enqueueBucket(files, timeoutMs, maxRetries);
|
|
36156
|
+
default:
|
|
36157
|
+
assertUnreachable(this.mode);
|
|
36158
|
+
}
|
|
36159
|
+
}
|
|
36160
|
+
/**
|
|
36161
|
+
* Flushes the current bucket immediately (bucket mode only).
|
|
36162
|
+
* In single/multiple mode this is a no-op.
|
|
36163
|
+
*/
|
|
36164
|
+
flush() {
|
|
36165
|
+
if (this.mode !== "bucket") return;
|
|
36166
|
+
if (this.currentBucket.length === 0) return;
|
|
36167
|
+
this.clearBucketTimer();
|
|
36168
|
+
this.flushBucketInternal("timer");
|
|
36169
|
+
}
|
|
36170
|
+
/**
|
|
36171
|
+
* Gracefully stops the sender. Rejects all queued items and
|
|
36172
|
+
* waits for active associations to complete.
|
|
36173
|
+
*/
|
|
36174
|
+
async stop() {
|
|
36175
|
+
if (this.isStopped) return;
|
|
36176
|
+
this.isStopped = true;
|
|
36177
|
+
if (this.options.signal !== void 0 && this.abortHandler !== void 0) {
|
|
36178
|
+
this.options.signal.removeEventListener("abort", this.abortHandler);
|
|
36179
|
+
}
|
|
36180
|
+
this.clearBucketTimer();
|
|
36181
|
+
this.rejectBucket("DicomSender: sender stopped");
|
|
36182
|
+
this.rejectQueue("DicomSender: sender stopped");
|
|
36183
|
+
await this.waitForActive();
|
|
36184
|
+
}
|
|
36185
|
+
/** Current sender status. */
|
|
36186
|
+
get status() {
|
|
36187
|
+
return {
|
|
36188
|
+
health: this.health,
|
|
36189
|
+
activeAssociations: this.activeAssociations,
|
|
36190
|
+
effectiveMaxAssociations: this.effectiveMaxAssociations,
|
|
36191
|
+
queueLength: this.queue.length,
|
|
36192
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
36193
|
+
consecutiveSuccesses: this.consecutiveSuccesses,
|
|
36194
|
+
stopped: this.isStopped
|
|
36195
|
+
};
|
|
36196
|
+
}
|
|
36197
|
+
// -----------------------------------------------------------------------
|
|
36198
|
+
// Typed event listener convenience methods
|
|
36199
|
+
// -----------------------------------------------------------------------
|
|
36200
|
+
/**
|
|
36201
|
+
* Registers a typed listener for a DicomSender-specific event.
|
|
36202
|
+
*
|
|
36203
|
+
* @param event - The event name from DicomSenderEventMap
|
|
36204
|
+
* @param listener - Callback receiving typed event data
|
|
36205
|
+
* @returns this for chaining
|
|
36206
|
+
*/
|
|
36207
|
+
onEvent(event, listener) {
|
|
36208
|
+
return this.on(event, listener);
|
|
36209
|
+
}
|
|
36210
|
+
/**
|
|
36211
|
+
* Registers a listener for successful sends.
|
|
36212
|
+
*
|
|
36213
|
+
* @param listener - Callback receiving send complete data
|
|
36214
|
+
* @returns this for chaining
|
|
36215
|
+
*/
|
|
36216
|
+
onSendComplete(listener) {
|
|
36217
|
+
return this.on("SEND_COMPLETE", listener);
|
|
36218
|
+
}
|
|
36219
|
+
/**
|
|
36220
|
+
* Registers a listener for failed sends.
|
|
36221
|
+
*
|
|
36222
|
+
* @param listener - Callback receiving send failed data
|
|
36223
|
+
* @returns this for chaining
|
|
36224
|
+
*/
|
|
36225
|
+
onSendFailed(listener) {
|
|
36226
|
+
return this.on("SEND_FAILED", listener);
|
|
36227
|
+
}
|
|
36228
|
+
/**
|
|
36229
|
+
* Registers a listener for health state changes.
|
|
36230
|
+
*
|
|
36231
|
+
* @param listener - Callback receiving health change data
|
|
36232
|
+
* @returns this for chaining
|
|
36233
|
+
*/
|
|
36234
|
+
onHealthChanged(listener) {
|
|
36235
|
+
return this.on("HEALTH_CHANGED", listener);
|
|
36236
|
+
}
|
|
36237
|
+
/**
|
|
36238
|
+
* Registers a listener for bucket flushes (bucket mode only).
|
|
36239
|
+
*
|
|
36240
|
+
* @param listener - Callback receiving bucket flush data
|
|
36241
|
+
* @returns this for chaining
|
|
36242
|
+
*/
|
|
36243
|
+
onBucketFlushed(listener) {
|
|
36244
|
+
return this.on("BUCKET_FLUSHED", listener);
|
|
36245
|
+
}
|
|
36246
|
+
// -----------------------------------------------------------------------
|
|
36247
|
+
// Single/Multiple mode: queue-based dispatch
|
|
36248
|
+
// -----------------------------------------------------------------------
|
|
36249
|
+
/** Enqueues a send and dispatches immediately if capacity allows. */
|
|
36250
|
+
enqueueSend(files, timeoutMs, maxRetries) {
|
|
36251
|
+
return new Promise((resolve) => {
|
|
36252
|
+
const totalQueued = this.queue.length + this.currentBucket.length;
|
|
36253
|
+
if (totalQueued >= this.maxQueueLength) {
|
|
36254
|
+
resolve(err(new Error("DicomSender: queue full")));
|
|
36255
|
+
return;
|
|
36256
|
+
}
|
|
36257
|
+
const entry = { files, timeoutMs, maxRetries, resolve };
|
|
36258
|
+
if (this.activeAssociations < this.effectiveMaxAssociations) {
|
|
36259
|
+
void this.executeEntry(entry);
|
|
36260
|
+
} else {
|
|
36261
|
+
this.queue.push(entry);
|
|
36262
|
+
}
|
|
36263
|
+
});
|
|
36264
|
+
}
|
|
36265
|
+
/** Drains queued entries up to available capacity. */
|
|
36266
|
+
drainQueue() {
|
|
36267
|
+
while (this.queue.length > 0 && this.activeAssociations < this.effectiveMaxAssociations) {
|
|
36268
|
+
const entry = this.queue.shift();
|
|
36269
|
+
if (entry === void 0) break;
|
|
36270
|
+
void this.executeEntry(entry);
|
|
36271
|
+
}
|
|
36272
|
+
}
|
|
36273
|
+
// -----------------------------------------------------------------------
|
|
36274
|
+
// Bucket mode: accumulate-then-flush
|
|
36275
|
+
// -----------------------------------------------------------------------
|
|
36276
|
+
/** Adds files to the current bucket and triggers flush if full. */
|
|
36277
|
+
enqueueBucket(files, timeoutMs, maxRetries) {
|
|
36278
|
+
return new Promise((resolve) => {
|
|
36279
|
+
const totalQueued = this.queue.length + this.currentBucket.length;
|
|
36280
|
+
if (totalQueued >= this.maxQueueLength) {
|
|
36281
|
+
resolve(err(new Error("DicomSender: queue full")));
|
|
36282
|
+
return;
|
|
36283
|
+
}
|
|
36284
|
+
this.currentBucket.push({ files, resolve, timeoutMs, maxRetries });
|
|
36285
|
+
const totalFiles = this.countBucketFiles();
|
|
36286
|
+
if (totalFiles >= this.maxBucketSize) {
|
|
36287
|
+
this.clearBucketTimer();
|
|
36288
|
+
void this.flushBucketInternal("maxSize");
|
|
36289
|
+
} else {
|
|
36290
|
+
this.resetBucketTimer();
|
|
36291
|
+
}
|
|
36292
|
+
});
|
|
36293
|
+
}
|
|
36294
|
+
/** Counts total files in the current bucket. */
|
|
36295
|
+
countBucketFiles() {
|
|
36296
|
+
let count = 0;
|
|
36297
|
+
for (let i = 0; i < this.currentBucket.length; i++) {
|
|
36298
|
+
count += this.currentBucket[i].files.length;
|
|
36299
|
+
}
|
|
36300
|
+
return count;
|
|
36301
|
+
}
|
|
36302
|
+
/** Flushes the current bucket: combines all files, dispatches as one send. */
|
|
36303
|
+
flushBucketInternal(reason) {
|
|
36304
|
+
if (this.currentBucket.length === 0) return;
|
|
36305
|
+
const entries = [...this.currentBucket];
|
|
36306
|
+
this.currentBucket = [];
|
|
36307
|
+
const allFiles = [];
|
|
36308
|
+
for (let i = 0; i < entries.length; i++) {
|
|
36309
|
+
for (let j = 0; j < entries[i].files.length; j++) {
|
|
36310
|
+
allFiles.push(entries[i].files[j]);
|
|
36311
|
+
}
|
|
36312
|
+
}
|
|
36313
|
+
let timeoutMs = 0;
|
|
36314
|
+
let maxRetries = 0;
|
|
36315
|
+
for (let i = 0; i < entries.length; i++) {
|
|
36316
|
+
if (entries[i].timeoutMs > timeoutMs) timeoutMs = entries[i].timeoutMs;
|
|
36317
|
+
if (entries[i].maxRetries > maxRetries) maxRetries = entries[i].maxRetries;
|
|
36318
|
+
}
|
|
36319
|
+
this.emit("BUCKET_FLUSHED", { fileCount: allFiles.length, reason });
|
|
36320
|
+
const bucketEntry = {
|
|
36321
|
+
files: allFiles,
|
|
36322
|
+
timeoutMs,
|
|
36323
|
+
maxRetries,
|
|
36324
|
+
resolve: (result) => {
|
|
36325
|
+
for (let i = 0; i < entries.length; i++) {
|
|
36326
|
+
entries[i].resolve(result);
|
|
36327
|
+
}
|
|
36328
|
+
}
|
|
36329
|
+
};
|
|
36330
|
+
if (this.activeAssociations < this.effectiveMaxAssociations) {
|
|
36331
|
+
void this.executeEntry(bucketEntry);
|
|
36332
|
+
} else {
|
|
36333
|
+
this.queue.push(bucketEntry);
|
|
36334
|
+
}
|
|
36335
|
+
}
|
|
36336
|
+
/** Resets the bucket flush timer. */
|
|
36337
|
+
resetBucketTimer() {
|
|
36338
|
+
this.clearBucketTimer();
|
|
36339
|
+
this.bucketTimer = setTimeout(() => {
|
|
36340
|
+
this.bucketTimer = void 0;
|
|
36341
|
+
void this.flushBucketInternal("timer");
|
|
36342
|
+
}, this.bucketFlushMs);
|
|
36343
|
+
}
|
|
36344
|
+
/** Clears the bucket flush timer. */
|
|
36345
|
+
clearBucketTimer() {
|
|
36346
|
+
if (this.bucketTimer !== void 0) {
|
|
36347
|
+
clearTimeout(this.bucketTimer);
|
|
36348
|
+
this.bucketTimer = void 0;
|
|
36349
|
+
}
|
|
36350
|
+
}
|
|
36351
|
+
// -----------------------------------------------------------------------
|
|
36352
|
+
// Core send execution with retry
|
|
36353
|
+
// -----------------------------------------------------------------------
|
|
36354
|
+
/** Executes a single queue entry: calls storescu with retry. */
|
|
36355
|
+
async executeEntry(entry) {
|
|
36356
|
+
this.activeAssociations++;
|
|
36357
|
+
const startMs = Date.now();
|
|
36358
|
+
const maxAttempts = entry.maxRetries + 1;
|
|
36359
|
+
const lastError = await this.attemptSend(entry, maxAttempts, startMs);
|
|
36360
|
+
if (lastError === void 0) return;
|
|
36361
|
+
this.activeAssociations--;
|
|
36362
|
+
this.recordFailure();
|
|
36363
|
+
this.emit("SEND_FAILED", { files: entry.files, error: lastError, attempts: maxAttempts });
|
|
36364
|
+
entry.resolve(err(lastError));
|
|
36365
|
+
this.drainQueue();
|
|
36366
|
+
}
|
|
36367
|
+
/** Attempts storescu up to maxAttempts times. Returns undefined on success, or the last error. */
|
|
36368
|
+
async attemptSend(entry, maxAttempts, startMs) {
|
|
36369
|
+
let lastError;
|
|
36370
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
36371
|
+
if (this.isStopped) {
|
|
36372
|
+
this.activeAssociations--;
|
|
36373
|
+
entry.resolve(err(new Error("DicomSender: sender stopped")));
|
|
36374
|
+
return void 0;
|
|
36375
|
+
}
|
|
36376
|
+
const result = await this.callStorescu(entry);
|
|
36377
|
+
if (result.ok) {
|
|
36378
|
+
this.handleSendSuccess(entry, startMs);
|
|
36379
|
+
return void 0;
|
|
36380
|
+
}
|
|
36381
|
+
lastError = result.error;
|
|
36382
|
+
if (attempt < maxAttempts - 1) {
|
|
36383
|
+
await delay2(this.retryDelayMs * (attempt + 1));
|
|
36384
|
+
}
|
|
36385
|
+
}
|
|
36386
|
+
return lastError ?? new Error("DicomSender: send failed");
|
|
36387
|
+
}
|
|
36388
|
+
/** Calls storescu with the configured options. */
|
|
36389
|
+
callStorescu(entry) {
|
|
36390
|
+
return storescu({
|
|
36391
|
+
host: this.options.host,
|
|
36392
|
+
port: this.options.port,
|
|
36393
|
+
files: [...entry.files],
|
|
36394
|
+
calledAETitle: this.options.calledAETitle,
|
|
36395
|
+
callingAETitle: this.options.callingAETitle,
|
|
36396
|
+
proposedTransferSyntax: this.options.proposedTransferSyntax,
|
|
36397
|
+
timeoutMs: entry.timeoutMs,
|
|
36398
|
+
signal: this.options.signal
|
|
36399
|
+
});
|
|
36400
|
+
}
|
|
36401
|
+
/** Handles a successful send: updates state, emits event, resolves promise. */
|
|
36402
|
+
handleSendSuccess(entry, startMs) {
|
|
36403
|
+
this.activeAssociations--;
|
|
36404
|
+
const durationMs = Date.now() - startMs;
|
|
36405
|
+
this.recordSuccess();
|
|
36406
|
+
this.emit("SEND_COMPLETE", { files: entry.files, fileCount: entry.files.length, durationMs });
|
|
36407
|
+
entry.resolve(ok({ files: entry.files, fileCount: entry.files.length, durationMs }));
|
|
36408
|
+
this.drainQueue();
|
|
36409
|
+
}
|
|
36410
|
+
// -----------------------------------------------------------------------
|
|
36411
|
+
// Backpressure state machine
|
|
36412
|
+
// -----------------------------------------------------------------------
|
|
36413
|
+
/** Records a successful send and adjusts health upward if needed. */
|
|
36414
|
+
recordSuccess() {
|
|
36415
|
+
this.consecutiveFailures = 0;
|
|
36416
|
+
this.consecutiveSuccesses++;
|
|
36417
|
+
if (this.health === SenderHealth.HEALTHY) return;
|
|
36418
|
+
if (this.consecutiveSuccesses >= RECOVERY_THRESHOLD) {
|
|
36419
|
+
this.consecutiveSuccesses = 0;
|
|
36420
|
+
const previousHealth = this.health;
|
|
36421
|
+
if (this.health === SenderHealth.DOWN) {
|
|
36422
|
+
this.health = SenderHealth.DEGRADED;
|
|
36423
|
+
} else {
|
|
36424
|
+
this.effectiveMaxAssociations = Math.min(this.effectiveMaxAssociations * 2, this.configuredMaxAssociations);
|
|
36425
|
+
if (this.effectiveMaxAssociations >= this.configuredMaxAssociations) {
|
|
36426
|
+
this.health = SenderHealth.HEALTHY;
|
|
36427
|
+
}
|
|
36428
|
+
}
|
|
36429
|
+
this.emitHealthChanged(previousHealth);
|
|
36430
|
+
this.drainQueue();
|
|
36431
|
+
}
|
|
36432
|
+
}
|
|
36433
|
+
/** Records a failed send and adjusts health downward if needed. */
|
|
36434
|
+
recordFailure() {
|
|
36435
|
+
this.consecutiveSuccesses = 0;
|
|
36436
|
+
this.consecutiveFailures++;
|
|
36437
|
+
const previousHealth = this.health;
|
|
36438
|
+
if (this.consecutiveFailures >= DOWN_THRESHOLD) {
|
|
36439
|
+
if (this.health !== SenderHealth.DOWN) {
|
|
36440
|
+
this.health = SenderHealth.DOWN;
|
|
36441
|
+
this.effectiveMaxAssociations = 1;
|
|
36442
|
+
this.emitHealthChanged(previousHealth);
|
|
36443
|
+
}
|
|
36444
|
+
} else if (this.consecutiveFailures >= DEGRADE_THRESHOLD && this.consecutiveFailures % DEGRADE_THRESHOLD === 0) {
|
|
36445
|
+
if (this.health === SenderHealth.HEALTHY) {
|
|
36446
|
+
this.health = SenderHealth.DEGRADED;
|
|
36447
|
+
this.effectiveMaxAssociations = Math.max(1, Math.floor(this.configuredMaxAssociations / 2));
|
|
36448
|
+
this.emitHealthChanged(previousHealth);
|
|
36449
|
+
} else if (this.health === SenderHealth.DEGRADED) {
|
|
36450
|
+
const newMax = Math.max(1, Math.floor(this.effectiveMaxAssociations / 2));
|
|
36451
|
+
if (newMax !== this.effectiveMaxAssociations) {
|
|
36452
|
+
this.effectiveMaxAssociations = newMax;
|
|
36453
|
+
this.emitHealthChanged(previousHealth);
|
|
36454
|
+
}
|
|
36455
|
+
}
|
|
36456
|
+
}
|
|
36457
|
+
}
|
|
36458
|
+
/** Emits a HEALTH_CHANGED event. */
|
|
36459
|
+
emitHealthChanged(previousHealth) {
|
|
36460
|
+
this.emit("HEALTH_CHANGED", {
|
|
36461
|
+
previousHealth,
|
|
36462
|
+
newHealth: this.health,
|
|
36463
|
+
effectiveMaxAssociations: this.effectiveMaxAssociations,
|
|
36464
|
+
consecutiveFailures: this.consecutiveFailures
|
|
36465
|
+
});
|
|
36466
|
+
}
|
|
36467
|
+
// -----------------------------------------------------------------------
|
|
36468
|
+
// Lifecycle helpers
|
|
36469
|
+
// -----------------------------------------------------------------------
|
|
36470
|
+
/** Rejects all queued entries with the given message. */
|
|
36471
|
+
rejectQueue(message) {
|
|
36472
|
+
while (this.queue.length > 0) {
|
|
36473
|
+
const entry = this.queue.shift();
|
|
36474
|
+
if (entry === void 0) break;
|
|
36475
|
+
entry.resolve(err(new Error(message)));
|
|
36476
|
+
}
|
|
36477
|
+
}
|
|
36478
|
+
/** Rejects all bucket entries with the given message. */
|
|
36479
|
+
rejectBucket(message) {
|
|
36480
|
+
while (this.currentBucket.length > 0) {
|
|
36481
|
+
const entry = this.currentBucket.shift();
|
|
36482
|
+
if (entry === void 0) break;
|
|
36483
|
+
entry.resolve(err(new Error(message)));
|
|
36484
|
+
}
|
|
36485
|
+
}
|
|
36486
|
+
/** Waits for all active associations to complete. */
|
|
36487
|
+
waitForActive() {
|
|
36488
|
+
if (this.activeAssociations === 0) return Promise.resolve();
|
|
36489
|
+
return new Promise((resolve) => {
|
|
36490
|
+
const check = () => {
|
|
36491
|
+
if (this.activeAssociations === 0) {
|
|
36492
|
+
resolve();
|
|
36493
|
+
} else {
|
|
36494
|
+
setTimeout(check, 50);
|
|
36495
|
+
}
|
|
36496
|
+
};
|
|
36497
|
+
check();
|
|
36498
|
+
});
|
|
36499
|
+
}
|
|
36500
|
+
// -----------------------------------------------------------------------
|
|
36501
|
+
// Abort signal
|
|
36502
|
+
// -----------------------------------------------------------------------
|
|
36503
|
+
/** Wires an AbortSignal to stop the sender. */
|
|
36504
|
+
wireAbortSignal(signal) {
|
|
36505
|
+
if (signal.aborted) {
|
|
36506
|
+
void this.stop();
|
|
36507
|
+
return;
|
|
36508
|
+
}
|
|
36509
|
+
this.abortHandler = () => {
|
|
36510
|
+
void this.stop();
|
|
36511
|
+
};
|
|
36512
|
+
signal.addEventListener("abort", this.abortHandler, { once: true });
|
|
36513
|
+
}
|
|
36514
|
+
};
|
|
36515
|
+
function delay2(ms) {
|
|
36516
|
+
return new Promise((resolve) => {
|
|
36517
|
+
setTimeout(resolve, ms);
|
|
36518
|
+
});
|
|
36519
|
+
}
|
|
36520
|
+
|
|
35995
36521
|
// src/pacs/types.ts
|
|
35996
36522
|
var QueryLevel = {
|
|
35997
36523
|
STUDY: "STUDY",
|
|
@@ -36541,7 +37067,7 @@ var DEFAULT_CONFIG = {
|
|
|
36541
37067
|
signal: void 0,
|
|
36542
37068
|
onRetry: void 0
|
|
36543
37069
|
};
|
|
36544
|
-
function
|
|
37070
|
+
function resolveConfig2(opts) {
|
|
36545
37071
|
if (!opts) return DEFAULT_CONFIG;
|
|
36546
37072
|
return {
|
|
36547
37073
|
...DEFAULT_CONFIG,
|
|
@@ -36586,7 +37112,7 @@ function shouldBreakAfterFailure(attempt, lastError, config) {
|
|
|
36586
37112
|
return false;
|
|
36587
37113
|
}
|
|
36588
37114
|
async function retry(operation, options) {
|
|
36589
|
-
const config =
|
|
37115
|
+
const config = resolveConfig2(options);
|
|
36590
37116
|
let lastResult = err(new Error("No attempts executed"));
|
|
36591
37117
|
for (let attempt = 0; attempt < config.maxAttempts; attempt += 1) {
|
|
36592
37118
|
lastResult = await operation();
|
|
@@ -36600,6 +37126,6 @@ async function retry(operation, options) {
|
|
|
36600
37126
|
return lastResult;
|
|
36601
37127
|
}
|
|
36602
37128
|
|
|
36603
|
-
export { AETitleSchema, AssociationTracker, ChangeSet, ColorConversion, DCMPRSCP_FATAL_EVENTS, DCMPRSCP_PATTERNS, DCMPSRCV_FATAL_EVENTS, DCMPSRCV_PATTERNS, DCMQRSCP_FATAL_EVENTS, DCMQRSCP_PATTERNS, DCMRECV_FATAL_EVENTS, DCMRECV_PATTERNS, DEFAULT_BLOCK_TIMEOUT_MS, DEFAULT_DICOM_PORT, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PARSE_CONCURRENCY, DEFAULT_START_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, Dcm2pnmOutputFormat, Dcm2xmlCharset, DcmQRSCP, DcmdumpFormat, Dcmj2pnmOutputFormat, DcmprsCP, DcmprscpEvent, Dcmpsrcv, DcmpsrcvEvent, DcmqrscpEvent, Dcmrecv, DcmrecvEvent, DcmtkProcess, DicomDataset, DicomInstance, DicomReceiver, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, Img2dcmInputFormat, JplsColorConversion, LineParser, LutType, MAX_BLOCK_LINES, MAX_CHANGESET_OPERATIONS, MAX_EVENT_PATTERNS, MAX_TRAVERSAL_DEPTH, MoveQueryModel, PDU_SIZE, PacsClient, PortSchema, PreferredTransferSyntax, ProcessState, ProposedTransferSyntax, QueryLevel, QueryModel, REQUIRED_BINARIES, RetrieveMode, SOP_CLASSES, STORESCP_FATAL_EVENTS, STORESCP_PATTERNS, StorageMode, StoreSCP, StoreSCPPreset, StorescpEvent, SubdirectoryMode, TransferSyntax, UIDSchema, UNIX_SEARCH_PATHS, VR, VR_CATEGORY, VR_CATEGORY_NAME, VR_META, WINDOWS_SEARCH_PATHS, WLMSCPFS_FATAL_EVENTS, WLMSCPFS_PATTERNS, Wlmscpfs, WlmscpfsEvent, assertUnreachable, batch, cda2dcm, clearDcmtkPathCache, createAETitle, createDicomFilePath, createDicomTag, createDicomTagPath, createPort, createSOPClassUID, createTransferSyntaxUID, dcm2cda, dcm2json, dcm2pdf, dcm2pnm, dcm2xml, dcmcjpeg, dcmcjpls, dcmconv, dcmcrle, dcmdecap, dcmdjpeg, dcmdjpls, dcmdrle, dcmdspfn, dcmdump, dcmencap, dcmftest, dcmgpdir, dcmj2pnm, dcmmkcrv, dcmmkdir, dcmmklut, dcmodify, dcmp2pgm, dcmprscu, dcmpschk, dcmpsmk, dcmpsprt, dcmqridx, dcmquant, dcmscale, dcmsend, dcod2lum, dconvlum, drtdump, dsr2xml, dsrdump, dump2dcm, echoscu, err, execCommand, findDcmtkPath, findscu, getVRCategory, getscu, img2dcm, isBinaryVR, isNumericVR, isStringVR, json2dcm, lookupTag, lookupTagByKeyword, lookupTagByName, mapResult, movescu, ok, parseAETitle, parseDicomTag, parseDicomTagPath, parsePort, parseSOPClassUID, parseTransferSyntaxUID, pdf2dcm, retry, segmentsToModifyPath, segmentsToString, sopClassNameFromUID, spawnCommand, stl2dcm, storescu, tag, tagPathToSegments, termscu, xml2dcm, xml2dsr, xmlToJson };
|
|
37129
|
+
export { AETitleSchema, AssociationTracker, ChangeSet, ColorConversion, DCMPRSCP_FATAL_EVENTS, DCMPRSCP_PATTERNS, DCMPSRCV_FATAL_EVENTS, DCMPSRCV_PATTERNS, DCMQRSCP_FATAL_EVENTS, DCMQRSCP_PATTERNS, DCMRECV_FATAL_EVENTS, DCMRECV_PATTERNS, DEFAULT_BLOCK_TIMEOUT_MS, DEFAULT_DICOM_PORT, DEFAULT_DRAIN_TIMEOUT_MS, DEFAULT_PARSE_CONCURRENCY, DEFAULT_START_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, Dcm2pnmOutputFormat, Dcm2xmlCharset, DcmQRSCP, DcmdumpFormat, Dcmj2pnmOutputFormat, DcmprsCP, DcmprscpEvent, Dcmpsrcv, DcmpsrcvEvent, DcmqrscpEvent, Dcmrecv, DcmrecvEvent, DcmtkProcess, DicomDataset, DicomInstance, DicomReceiver, DicomSender, DicomTagPathSchema, DicomTagSchema, FilenameMode, GetQueryModel, Img2dcmInputFormat, JplsColorConversion, LineParser, LutType, MAX_BLOCK_LINES, MAX_CHANGESET_OPERATIONS, MAX_EVENT_PATTERNS, MAX_TRAVERSAL_DEPTH, MoveQueryModel, PDU_SIZE, PacsClient, PortSchema, PreferredTransferSyntax, ProcessState, ProposedTransferSyntax, QueryLevel, QueryModel, REQUIRED_BINARIES, RetrieveMode, SOP_CLASSES, STORESCP_FATAL_EVENTS, STORESCP_PATTERNS, SenderHealth, SenderMode, StorageMode, StoreSCP, StoreSCPPreset, StorescpEvent, SubdirectoryMode, TransferSyntax, UIDSchema, UNIX_SEARCH_PATHS, VR, VR_CATEGORY, VR_CATEGORY_NAME, VR_META, WINDOWS_SEARCH_PATHS, WLMSCPFS_FATAL_EVENTS, WLMSCPFS_PATTERNS, Wlmscpfs, WlmscpfsEvent, assertUnreachable, batch, cda2dcm, clearDcmtkPathCache, createAETitle, createDicomFilePath, createDicomTag, createDicomTagPath, createPort, createSOPClassUID, createTransferSyntaxUID, dcm2cda, dcm2json, dcm2pdf, dcm2pnm, dcm2xml, dcmcjpeg, dcmcjpls, dcmconv, dcmcrle, dcmdecap, dcmdjpeg, dcmdjpls, dcmdrle, dcmdspfn, dcmdump, dcmencap, dcmftest, dcmgpdir, dcmj2pnm, dcmmkcrv, dcmmkdir, dcmmklut, dcmodify, dcmp2pgm, dcmprscu, dcmpschk, dcmpsmk, dcmpsprt, dcmqridx, dcmquant, dcmscale, dcmsend, dcod2lum, dconvlum, drtdump, dsr2xml, dsrdump, dump2dcm, echoscu, err, execCommand, findDcmtkPath, findscu, getVRCategory, getscu, img2dcm, isBinaryVR, isNumericVR, isStringVR, json2dcm, lookupTag, lookupTagByKeyword, lookupTagByName, mapResult, movescu, ok, parseAETitle, parseDicomTag, parseDicomTagPath, parsePort, parseSOPClassUID, parseTransferSyntaxUID, pdf2dcm, retry, segmentsToModifyPath, segmentsToString, sopClassNameFromUID, spawnCommand, stl2dcm, storescu, tag, tagPathToSegments, termscu, xml2dcm, xml2dsr, xmlToJson };
|
|
36604
37130
|
//# sourceMappingURL=index.js.map
|
|
36605
37131
|
//# sourceMappingURL=index.js.map
|