@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/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 = /^[A-Za-z0-9 -]+$/;
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 letters, digits, spaces, and hyphens are allowed`));
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 resolveConfig(opts) {
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 = resolveConfig(options);
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