@ubercode/dcmtk 0.11.0 → 0.12.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/servers.cjs CHANGED
@@ -2855,6 +2855,9 @@ var Worker = class {
2855
2855
  __publicField(this, "_state", "idle");
2856
2856
  __publicField(this, "_context");
2857
2857
  __publicField(this, "_pending", /* @__PURE__ */ new Set());
2858
+ __publicField(this, "_instancePending", /* @__PURE__ */ new Set());
2859
+ __publicField(this, "_instancesReceived", 0);
2860
+ __publicField(this, "_instanceErrors", 0);
2858
2861
  __publicField(this, "_files", []);
2859
2862
  __publicField(this, "_fileSizes", []);
2860
2863
  __publicField(this, "_outputLines", []);
@@ -2892,6 +2895,9 @@ var Worker = class {
2892
2895
  this._fileSizes.length = 0;
2893
2896
  this._outputLines.length = 0;
2894
2897
  this._pending.clear();
2898
+ this._instancePending.clear();
2899
+ this._instancesReceived = 0;
2900
+ this._instanceErrors = 0;
2895
2901
  }
2896
2902
  /** Returns the worker to idle and clears all association state. */
2897
2903
  endAssociation() {
@@ -2901,6 +2907,9 @@ var Worker = class {
2901
2907
  this._fileSizes.length = 0;
2902
2908
  this._outputLines.length = 0;
2903
2909
  this._pending.clear();
2910
+ this._instancePending.clear();
2911
+ this._instancesReceived = 0;
2912
+ this._instanceErrors = 0;
2904
2913
  this._remoteSocket = void 0;
2905
2914
  this._workerSocket = void 0;
2906
2915
  }
@@ -2911,6 +2920,35 @@ var Worker = class {
2911
2920
  this._pending.delete(promise);
2912
2921
  });
2913
2922
  }
2923
+ /** Tracks an in-flight instance parsing promise; auto-removes on completion. */
2924
+ trackInstance(promise) {
2925
+ this._instancePending.add(promise);
2926
+ void promise.finally(() => {
2927
+ this._instancePending.delete(promise);
2928
+ });
2929
+ }
2930
+ /** Records a successful instance parse. */
2931
+ recordInstanceSuccess() {
2932
+ this._instancesReceived++;
2933
+ }
2934
+ /** Records a failed instance parse. */
2935
+ recordInstanceError() {
2936
+ this._instanceErrors++;
2937
+ }
2938
+ /** Awaits all in-flight instance parsing promises. */
2939
+ async drainInstancePending() {
2940
+ if (this._instancePending.size > 0) {
2941
+ await Promise.all(this._instancePending);
2942
+ }
2943
+ }
2944
+ /** Number of successfully parsed instances in this association. */
2945
+ get instancesReceived() {
2946
+ return this._instancesReceived;
2947
+ }
2948
+ /** Number of failed instance parses in this association. */
2949
+ get instanceErrors() {
2950
+ return this._instanceErrors;
2951
+ }
2914
2952
  /** Records a successfully received file path and its size. */
2915
2953
  recordFile(filePath, size) {
2916
2954
  this._files.push(filePath);
@@ -3082,7 +3120,16 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3082
3120
  return this.on("INSTANCE_RECEIVED", listener);
3083
3121
  }
3084
3122
  /**
3085
- * Registers a listener for completed associations.
3123
+ * Registers a listener for DicomInstance.open failures.
3124
+ *
3125
+ * @param listener - Callback receiving instance error data
3126
+ * @returns this for chaining
3127
+ */
3128
+ onInstanceError(listener) {
3129
+ return this.on("INSTANCE_ERROR", listener);
3130
+ }
3131
+ /**
3132
+ * Registers a listener for completed associations (file moves done).
3086
3133
  *
3087
3134
  * @param listener - Callback receiving association data
3088
3135
  * @returns this for chaining
@@ -3090,6 +3137,15 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3090
3137
  onAssociationComplete(listener) {
3091
3138
  return this.on("ASSOCIATION_COMPLETE", listener);
3092
3139
  }
3140
+ /**
3141
+ * Registers a listener for fully finalized associations (all parsing done).
3142
+ *
3143
+ * @param listener - Callback receiving finalized data with instance counts
3144
+ * @returns this for chaining
3145
+ */
3146
+ onAssociationFinalized(listener) {
3147
+ return this.on("ASSOCIATION_FINALIZED", listener);
3148
+ }
3093
3149
  /**
3094
3150
  * Registers a listener for new associations (bubbled from workers).
3095
3151
  *
@@ -3419,34 +3475,33 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3419
3475
  calledAE: data.calledAE,
3420
3476
  source: data.source
3421
3477
  });
3422
- this.emitInstanceReceived(finalPath, fileSize, data, ctx);
3478
+ const instanceCtx = {
3479
+ filePath: finalPath,
3480
+ fileSize,
3481
+ associationId: ctx.associationId,
3482
+ associationDir: ctx.associationDir,
3483
+ callingAE: data.callingAE,
3484
+ calledAE: data.calledAE,
3485
+ source: data.source
3486
+ };
3487
+ worker.trackInstance(this.parseAndEmitInstance(worker, instanceCtx));
3423
3488
  }
3424
- /** Parses a DICOM file and emits INSTANCE_RECEIVED or error. */
3425
- emitInstanceReceived(filePath, fileSize, data, ctx) {
3426
- void DicomInstance.open(filePath).then((openResult) => {
3489
+ /** Parses a DICOM file and emits INSTANCE_RECEIVED or INSTANCE_ERROR. */
3490
+ async parseAndEmitInstance(worker, ctx) {
3491
+ try {
3492
+ const openResult = await DicomInstance.open(ctx.filePath);
3427
3493
  if (!openResult.ok) {
3428
- this.emit("error", {
3429
- error: openResult.error,
3430
- filePath,
3431
- associationId: ctx.associationId,
3432
- associationDir: ctx.associationDir,
3433
- callingAE: data.callingAE,
3434
- calledAE: data.calledAE,
3435
- source: data.source
3436
- });
3494
+ worker.recordInstanceError();
3495
+ this.emit("INSTANCE_ERROR", { error: openResult.error, thrown: false, ...ctx });
3437
3496
  return;
3438
3497
  }
3439
- this.emit("INSTANCE_RECEIVED", {
3440
- filePath,
3441
- fileSize,
3442
- associationId: ctx.associationId,
3443
- associationDir: ctx.associationDir,
3444
- callingAE: data.callingAE,
3445
- calledAE: data.calledAE,
3446
- source: data.source,
3447
- instance: openResult.value
3448
- });
3449
- });
3498
+ worker.recordInstanceSuccess();
3499
+ this.emit("INSTANCE_RECEIVED", { ...ctx, instance: openResult.value });
3500
+ } catch (thrown) {
3501
+ const error = thrown instanceof Error ? thrown : new Error(String(thrown));
3502
+ worker.recordInstanceError();
3503
+ this.emit("INSTANCE_ERROR", { error, thrown: true, ...ctx });
3504
+ }
3450
3505
  }
3451
3506
  /** Returns worker to idle pool on association complete, emits summary. */
3452
3507
  wireAssociationComplete(worker) {
@@ -3454,10 +3509,12 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3454
3509
  void this.finalizeAssociation(worker, data);
3455
3510
  });
3456
3511
  }
3457
- /** Awaits ALL pending file operations, emits ASSOCIATION_COMPLETE, resets worker state. */
3512
+ /** Awaits file ops, emits ASSOCIATION_COMPLETE, awaits parsing, emits ASSOCIATION_FINALIZED. */
3458
3513
  async finalizeAssociation(worker, data) {
3459
3514
  await worker.drainPendingFiles();
3460
3515
  this.emitAssociationComplete(worker, data);
3516
+ await worker.drainInstancePending();
3517
+ this.emitAssociationFinalized(worker, data);
3461
3518
  worker.endAssociation();
3462
3519
  void this.scaleDown();
3463
3520
  }
@@ -3489,6 +3546,20 @@ var DicomReceiver = class _DicomReceiver extends events.EventEmitter {
3489
3546
  output
3490
3547
  });
3491
3548
  }
3549
+ /** Emits ASSOCIATION_FINALIZED after all instance parsing is done. */
3550
+ emitAssociationFinalized(worker, data) {
3551
+ const ctx = worker.context;
3552
+ this.emit("ASSOCIATION_FINALIZED", {
3553
+ associationId: ctx?.associationId ?? data.associationId,
3554
+ associationDir: ctx?.associationDir ?? "",
3555
+ callingAE: data.callingAE,
3556
+ calledAE: data.calledAE,
3557
+ source: data.source,
3558
+ files: [...worker.files],
3559
+ instancesReceived: worker.instancesReceived,
3560
+ instanceErrors: worker.instanceErrors
3561
+ });
3562
+ }
3492
3563
  /** Bubbles ASSOCIATION_RECEIVED from dcmrecv worker. */
3493
3564
  wireAssociationReceived(worker) {
3494
3565
  worker.dcmrecv.onEvent("ASSOCIATION_RECEIVED", (data) => {