All files / extensions/cornerstone/src/services/ViewportService/backends dataIdRegistry.ts

90% Statements 27/30
68.75% Branches 11/16
80% Functions 4/5
90% Lines 27/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122                                                                    178x     178x   178x                                                         4x 4x   4x     4x 4x 4x   4x   4x 4x 4x               167x 167x 165x   2x 2x 2x 2x 2x 2x         2x                 9x     9x 9x 9x      
import { utilities as csUtils } from '@cornerstonejs/core';
 
/**
 * Payload registered with cornerstone's global GenericViewport dataset metadata
 * provider. The shape is family-specific and mirrors what each native viewport's
 * data provider reads (see the cornerstone genericViewport examples):
 *   - planar : imageIds (+ optional volumeId) โ€” stack / volume-slice / MPR / 3D
 *   - video  : sourceDataId (the video imageId)
 *   - ecg    : sourceDataId (the waveform imageId)
 *   - wsi    : imageIds + a DICOMweb webClient used to fetch tiles
 */
export type DataIdPayload =
  | { kind: 'planar'; imageIds: string[]; volumeId?: string; initialImageIdIndex?: number }
  | { kind: 'video' | 'ecg'; sourceDataId: string }
  | { kind: 'wsi'; imageIds: string[]; options: { webClient: unknown } };
 
/**
 * Owns the lifecycle of OHIF's `dataId` registrations against cornerstone's
 * process-global `genericViewportDisplaySetMetadataProvider` (see migration plan ยง4.7).
 *
 * Why this exists: cornerstone's `removeData`/`setDisplaySets` do NOT garbage-collect
 * the global registration store (upstream blocker CS-18), so OHIF must own add/remove.
 * The MPR triptych mounts the SAME `dataId` (the displaySetInstanceUID-derived volume)
 * from N panes, so a naive add-on-every-mount / never-remove both over-registers and
 * leaks. This registry ref-counts per `dataId` and keeps a per-viewport ledger so:
 *   - `provider.add` fires only on the 0 -> 1 transition,
 *   - `provider.remove` fires only on the 1 -> 0 transition,
 *   - unmounting one pane of a shared-volume triptych does not unregister data the
 *     other panes still need.
 *
 * Used by the native ("next") backend for all families, and by the legacy
 * backend for its one provider-backed family (WSI mounts via mountOther).
 */
export class DataIdRegistry {
  // Global ref-count keyed by dataId (the provider store is a single global namespace).
  private readonly refCounts = new Map<string, number>();
  // Last payload registered per dataId, so a re-registration that promotes a
  // stack-only dataId to volume-backed can be detected and forwarded to the provider.
  private readonly payloads = new Map<string, DataIdPayload>();
  // Per-viewport ledger of the dataIds it registered, to drive release on unmount.
  private readonly byViewport = new Map<string, string[]>();
 
  /**
   * Builds the registration dataId for a display set. PT/CT *fusion* overlays are
   * distinct display sets with their own UIDs, so the bare displaySetInstanceUID is
   * already collision-free and is used for both source and overlay bindings (the LUT
   * presentation store keys by the same UID, giving a clean 1:1 dataId mapping). The
   * `'overlay'` suffix is reserved for the case where a source and an overlay share the
   * SAME displaySetInstanceUID but need distinct registrations โ€” i.e. derived labelmap
   * overlays (segmentation / M4), which are not yet on the native path.
   */
  static dataIdFor(displaySetInstanceUID: string, role?: 'overlay'): string {
    return role === 'overlay' ? `${displaySetInstanceUID}::overlay` : displaySetInstanceUID;
  }
 
  /**
   * Registers (or ref-bumps) a dataId for a viewport. Adds to the cornerstone
   * provider only on the first reference. Idempotent payloads across panes that
   * share a dataId are expected (same imageIds/volumeId), so first-writer wins โ€”
   * EXCEPT when a later payload promotes a previously stack-only registration to
   * a volume-backed one (gains a `volumeId`). That happens when a data overlay
   * (fusion) is added to a viewport whose source was first mounted as a vtkImage
   * stack: the source is re-registered with its volumeId so it can render as a
   * volume slice alongside the overlay. Without updating the provider here, the
   * source would keep its volumeId-less payload and stay vtkImage while the
   * overlay is a vtkVolumeSlice (broken fusion).
   */
  register(viewportId: string, dataId: string, payload: DataIdPayload): void {
    const prev = this.refCounts.get(dataId) ?? 0;
    const existing = this.payloads.get(dataId);
    const promotesToVolume =
      !!(payload as { volumeId?: string }).volumeId &&
      !(existing as { volumeId?: string } | undefined)?.volumeId;
 
    if (prev === 0 || promotesToVolume) {
      csUtils.genericViewportDisplaySetMetadataProvider.add(dataId, payload);
      this.payloads.set(dataId, payload);
    }
    this.refCounts.set(dataId, prev + 1);
 
    const list = this.byViewport.get(viewportId) ?? [];
    list.push(dataId);
    this.byViewport.set(viewportId, list);
  }
 
  /**
   * Releases every dataId a viewport registered (called on element disable).
   * Removes from the provider only when the last reference is gone.
   */
  releaseViewport(viewportId: string): void {
    const dataIds = this.byViewport.get(viewportId);
    if (!dataIds) {
      return;
    }
    for (const dataId of dataIds) {
      const next = (this.refCounts.get(dataId) ?? 1) - 1;
      if (next <= 0) {
        this.refCounts.delete(dataId);
        this.payloads.delete(dataId);
        csUtils.genericViewportDisplaySetMetadataProvider.remove(dataId);
      } else E{
        this.refCounts.set(dataId, next);
      }
    }
    this.byViewport.delete(viewportId);
  }
 
  /**
   * Flushes all remaining registrations (called on service destroy). Removes each
   * dataId individually rather than `provider.clear()`, which would wipe
   * registrations owned by other rendering contexts / service instances.
   */
  destroy(): void {
    for (const dataId of this.refCounts.keys()) {
      csUtils.genericViewportDisplaySetMetadataProvider.remove(dataId);
    }
    this.refCounts.clear();
    this.payloads.clear();
    this.byViewport.clear();
  }
}