← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/sequence/interface-state.ts

4.52 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import { z } from "zod";

import { SEQUENCE_VIEWER_LIMITS } from "../runtime-contract";
import {
  createFastqQualityViewState,
  FASTQ_QUALITY_REPORT_LIMITS,
  FASTQ_QUALITY_TABLE_IDS,
} from "./fastq-quality-analysis";
import {
  DEFAULT_READ_PILEUP_STATE,
  getReadPileupEntry,
  getReadPileupEntryForRecord,
} from "./read-pileup";
import type { SequenceTrack } from "./tracks";
import type { SequenceRecord } from "./types";

const nonnegativeSafeInteger = z.number().int().nonnegative().safe();
const browserQuery = z.string().max(500);
const qualityTableIds = z
  .array(z.enum(FASTQ_QUALITY_TABLE_IDS))
  .max(FASTQ_QUALITY_TABLE_IDS.length)
  .refine(
    (ids) => new Set(ids).size === ids.length,
    "Expanded quality tables must be unique.",
  );

/** Presentation settings only; computed reports and source arrays are not state. */
export const sequenceInterfaceSettingsSchema = z
  .object({
    annotationIndex: z
      .object({
        expanded: z.boolean(),
        page: nonnegativeSafeInteger,
        query: browserQuery,
      })
      .strict(),
    chromatogram: z
      .object({
        basesPerWindow: z.number().int().min(1).max(100),
        firstBase: z
          .number()
          .int()
          .positive()
          .max(SEQUENCE_VIEWER_LIMITS.input.maxTotalResidues),
      })
      .strict(),
    originRangeExpanded: z.boolean(),
    quality: z
      .object({
        adapterSequence: z
          .string()
          .min(8)
          .max(FASTQ_QUALITY_REPORT_LIMITS.maxAdapterBases)
          .regex(/^[ACGT]+$/u)
          .nullable(),
        view: z
          .object({
            distributionsExpanded: z.boolean(),
            expandedTables: qualityTableIds,
            methodsExpanded: z.boolean(),
          })
          .strict(),
      })
      .strict(),
    readPileup: z
      .object({
        options: z
          .object({
            includeDuplicates: z.boolean(),
            includeQcFailed: z.boolean(),
            includeSecondary: z.boolean(),
            includeSupplementary: z.boolean(),
            includeUnknownMappingQuality: z.boolean(),
            minimumMappingQuality: z.number().int().min(0).max(255),
            showAllBases: z.boolean(),
            showSoftClips: z.boolean(),
            sortBy: z.enum(["position", "mapping-quality", "strand"]),
            strand: z.enum(["all", "+", "-"]),
          })
          .strict(),
        selectedRead: z
          .object({
            sourceReadIndex: nonnegativeSafeInteger,
            trackId: z.string().min(1).max(2_000),
          })
          .strict()
          .nullable(),
      })
      .strict(),
    recordBrowser: z
      .object({
        expanded: z.boolean(),
        page: nonnegativeSafeInteger,
        query: browserQuery,
        sortBy: z.enum(["source", "label", "length", "molecule"]),
      })
      .strict(),
  })
  .strict();

export type SequenceInterfaceSettings = z.infer<
  typeof sequenceInterfaceSettingsSchema
>;

export function createSequenceInterfaceSettings(): SequenceInterfaceSettings {
  return {
    annotationIndex: { expanded: false, page: 0, query: "" },
    chromatogram: { basesPerWindow: 40, firstBase: 1 },
    originRangeExpanded: false,
    quality: { adapterSequence: null, view: createFastqQualityViewState() },
    readPileup: {
      options: { ...DEFAULT_READ_PILEUP_STATE.options },
      selectedRead: null,
    },
    recordBrowser: { expanded: false, page: 0, query: "", sortBy: "source" },
  };
}

/** Check exact retained-read identity even when a source-relative session has no snapshot. */
export function validateSequenceInterfaceSettingsForSource({
  record,
  records,
  settings,
  tracks,
}: {
  record?: Pick<
    SequenceRecord,
    "evidenceCoordinatesStale" | "length" | "sourceLabel"
  >;
  records?: ReadonlyArray<Pick<SequenceRecord, "sourceLabel">>;
  settings: SequenceInterfaceSettings;
  tracks: ReadonlyArray<SequenceTrack>;
}): void {
  if (
    record != null &&
    settings.chromatogram.firstBase > Math.max(1, record.length)
  ) {
    throw new Error(
      "The saved chromatogram window is outside its selected source record.",
    );
  }
  const selectedRead = settings.readPileup.selectedRead;
  if (selectedRead == null) return;
  try {
    if (record == null) getReadPileupEntry(tracks, selectedRead);
    else getReadPileupEntryForRecord(tracks, selectedRead, record, records);
  } catch (error) {
    throw new Error(
      `The saved read selection cannot be restored. ${
        error instanceof Error
          ? error.message
          : "The source identity is invalid."
      }`,
    );
  }
}

SHA-256: a93ca240c75ed726523e4a6fe5b02e9f6508823d48cf9ebab43b21340dedb67f