All files / tools/src/utilities safeStructuredClone.ts

92.85% Statements 26/28
91.66% Branches 22/24
100% Functions 5/5
92.59% Lines 25/27

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      430x         175x     175x 175x                                       430x                         1345x 1345x 4218x 366x 366x 175x   366x   3852x 1496x 2356x 1210x 4523x     1146x     1345x                           9312x     9312x 7583x   1729x 4590x   199x    
import type { Types } from '@cornerstonejs/core';
import { utilities as csUtils } from '@cornerstonejs/core';
 
const { PointsManager } = csUtils;
 
type OmitKeyHandler = ((key: string, value: unknown) => unknown) | null;
 
function cloneContourValue(_key: string, value: unknown): unknown {
  Iif (value == null || typeof value !== 'object' || !('polyline' in value)) {
    return value;
  }
  const contour = value as { polyline: unknown[]; [k: string]: unknown };
  return {
    ...contour,
    polyline: null,
    pointsManager: PointsManager.create3(
      contour.polyline.length,
      contour.polyline as Types.Point3[]
    ),
  };
}
 
/**
 * Keys that are known to hold large or non-cloneable data (e.g. in annotation
 * data or cachedStats). When the value is null, the key is omitted. When the
 * value is a function, it is called with (key, value) and the return value is
 * used as the new value for that key.
 * - pointsInVolume: large array of Point3 (e.g. CircleROIStartEndThresholdTool)
 * - projectionPoints: array of arrays of Point3 (e.g. CircleROIStartEndThresholdTool)
 * - contour: cloneable via polyline → pointsManager (handled by cloneContourValue)
 * - spline: annotation-specific object with non-cloneable refs (omitted)
 */
const OMIT_KEYS = new Map<string, OmitKeyHandler>([
  ['pointsInVolume', null],
  ['projectionPoints', null],
  ['contour', cloneContourValue],
  ['spline', null],
]);
 
/**
 * Recursively copies an object, omitting known large/non-cloneable keys.
 */
function omitUncloneableKeys(
  obj: Record<string, unknown>
): Record<string, unknown> {
  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(obj)) {
    if (OMIT_KEYS.has(key)) {
      const handler = OMIT_KEYS.get(key);
      if (handler) {
        result[key] = handler(key, value);
      }
      continue;
    }
    if (value === null || value === undefined || typeof value !== 'object') {
      result[key] = value;
    } else if (Array.isArray(value)) {
      result[key] = value.map((value) =>
        safeStructuredClone(value as Record<string, unknown>)
      );
    } else {
      result[key] = omitUncloneableKeys(value as Record<string, unknown>);
    }
  }
  return result;
}
 
/**
 * Like structuredClone, but safe for annotation-style data: omits known large
 * or non-cloneable keys (e.g. pointsInVolume, projectionPoints) and replaces
 * any value that cannot be cloned with null if the whole clone fails.
 * Use for cloning annotation data or other objects that may contain large
 * arrays or non-cloneable references.
 *
 * @param value - Any value (typically annotation data object).
 * @returns Deep copy with omit-keys stripped and uncloneable values nulled on fallback.
 */
export function safeStructuredClone<T>(value: T): T {
  Iif (value === null || value === undefined) {
    return value;
  }
  if (typeof value !== 'object') {
    return value;
  }
  if (Array.isArray(value)) {
    return value.map((item) => safeStructuredClone(item)) as unknown as T;
  }
  return omitUncloneableKeys(value as Record<string, unknown>) as T;
}