All files / src errors.ts

100% Statements 26/26
100% Branches 12/12
100% Functions 5/5
100% Lines 26/26

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                  2x                                                             2x 2x 860x   2x 860x 860x 860x 860x 860x 2x                     2x 9x 8x 9x 9x     2x 2x 16x 16x 16x 2x                         2x 106x 34x 34x  
/**
 * Typed refusals.
 *
 * Spec §10: every refusal names the file's actual state and the concrete fix.
 * The `code` is what the UI switches on; the `message` is what the user reads;
 * `detail` carries the machine-readable actual value so a test can assert on it
 * without regex-matching prose.
 */
 
import { pyList } from './format'
 
export type ErrorCode =
  | 'not-hdf5'
  | 'not-a-table'
  | 'not-a-cooler'
  | 'mmetacool-root'
  | 'mcool-root'
  | 'pairs-binning-needed'
  | 'entry-not-found'
  | 'bad-format-version'
  | 'bad-ordering'
  | 'variable-bins'
  | 'unbinned-not-displayable'
  | 'dense-too-large'
  | 'unknown-label'
  | 'cell-not-found'
  | 'scool-listing-cost'
  | 'hicpro-load-cost'
  | 'hicpro-no-bins'
  | 'bad-range'
  | 'genome-coordinate-suffix'
  | 'unsupported'
  | 'bigwig-no-overlap'
  | 'remote-no-ranges'
  | 'remote-compressed'
  | 'remote-cors'
  | 'remote-too-large'
  | 'bad-uri'
  | 'format'
 
export class ViewerError extends Error {
  readonly code: ErrorCode
  readonly detail: Record<string, unknown>
 
  constructor(code: ErrorCode, message: string, detail: Record<string, unknown> = {}) {
    super(message)
    this.name = 'ViewerError'
    this.code = code
    this.detail = detail
  }
}
 
/**
 * Whether an error is the browser refusing memory — a typed array it could not
 * allocate — rather than anything about the file.
 *
 * Read off the name and the words, because that is all that crosses the worker
 * boundary (`worker/protocol.ts` keeps both) and no engine gives it a code:
 * Chrome says *Array buffer allocation failed*, Firefox *out of memory* or
 * *invalid array length*, Safari *Out of memory*.
 */
export function isAllocationFailure(err: unknown): boolean {
  if (!(err instanceof Error)) return false
  const words = /allocation failed|out of memory|invalid array length|array buffer/i
  return (err.name === 'RangeError' || err.name === 'InternalError') && words.test(err.message)
}
 
/** Raised when a file does not conform to the metacooler format. */
export class MetaCoolerFormatError extends ViewerError {
  constructor(message: string, detail: Record<string, unknown> = {}, code: ErrorCode = 'format') {
    super(code, message, detail)
    this.name = 'MetaCoolerFormatError'
  }
}
 
/**
 * Render a list of names the way the Python errors do: at most `limit` entries,
 * then a count of the remainder.
 *
 * A port of `_truncate` in `ops/range.py`, down to the quoting.
 * `JSON.stringify` would render `["a","b"]` where Python's `str(list)` renders
 * `['a', 'b']` — different quotes *and* no space after the comma — and these
 * strings end up inside the `unknown-label` refusal, which spec §10 requires
 * to read identically in the browser and in `metacooler show`. A user grepping
 * for a name they saw in one must find it in the other.
 */
export function truncateNames(names: readonly string[], limit = 20): string {
  if (names.length <= limit) return pyList(names)
  return `${pyList(names.slice(0, limit))} ... (${names.length - limit} more)`
}