loom-browser 0.0.13 → 0.0.15

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.
@@ -5759,15 +5759,15 @@ class TwoBitFile {
5759
5759
  */
5760
5760
  // ─── Internal helpers ─────────────────────────────────────────────────────────
5761
5761
  // Cache TwoBitFile instances by URL to reuse parsed headers and indices.
5762
- const readerCache$4 = new Map();
5762
+ const readerCache$5 = new Map();
5763
5763
  // Parallel filehandle cache for direct batched reads (bypasses @gmod/twobit).
5764
5764
  const filehandleCache = new Map();
5765
- function getReader$3(url, fetchImpl) {
5766
- let reader = readerCache$4.get(url);
5765
+ function getReader$4(url, fetchImpl) {
5766
+ let reader = readerCache$5.get(url);
5767
5767
  if (!reader) {
5768
5768
  const filehandle = getFilehandle(url, fetchImpl);
5769
5769
  reader = new TwoBitFile({ filehandle });
5770
- readerCache$4.set(url, reader);
5770
+ readerCache$5.set(url, reader);
5771
5771
  }
5772
5772
  return reader;
5773
5773
  }
@@ -5796,14 +5796,14 @@ function getFilehandle(url, fetchImpl) {
5796
5796
  function createTwoBitSequenceProvider(url, fetchImpl) {
5797
5797
  return async (locus) => {
5798
5798
  var _a;
5799
- const reader = getReader$3(url, fetchImpl);
5799
+ const reader = getReader$4(url, fetchImpl);
5800
5800
  try {
5801
5801
  const sequence = await reader.getSequence(locus.chr, locus.start, locus.end);
5802
5802
  return sequence !== null && sequence !== void 0 ? sequence : '';
5803
5803
  }
5804
5804
  catch (err) {
5805
5805
  if (err instanceof Error && (err.name === 'AbortError' || ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('aborted')))) {
5806
- readerCache$4.delete(url);
5806
+ readerCache$5.delete(url);
5807
5807
  filehandleCache.delete(url);
5808
5808
  }
5809
5809
  throw err;
@@ -9703,10 +9703,10 @@ class BgzipIndexedFasta extends IndexedFasta {
9703
9703
  */
9704
9704
  // ─── Internal helpers ─────────────────────────────────────────────────────────
9705
9705
  // Cache reader instances by URL to reuse parsed indices.
9706
- const readerCache$3 = new Map();
9707
- function getReader$2(fastaURL, indexURL, compressedIndexURL, fetchImpl) {
9706
+ const readerCache$4 = new Map();
9707
+ function getReader$3(fastaURL, indexURL, compressedIndexURL, fetchImpl) {
9708
9708
  const cacheKey = fastaURL;
9709
- let reader = readerCache$3.get(cacheKey);
9709
+ let reader = readerCache$4.get(cacheKey);
9710
9710
  if (!reader) {
9711
9711
  const fileOpts = fetchImpl ? { fetch: fetchImpl } : undefined;
9712
9712
  if (compressedIndexURL) {
@@ -9722,7 +9722,7 @@ function getReader$2(fastaURL, indexURL, compressedIndexURL, fetchImpl) {
9722
9722
  fai: new RemoteFile(indexURL, fileOpts),
9723
9723
  });
9724
9724
  }
9725
- readerCache$3.set(cacheKey, reader);
9725
+ readerCache$4.set(cacheKey, reader);
9726
9726
  }
9727
9727
  return reader;
9728
9728
  }
@@ -9742,14 +9742,14 @@ function createFastaSequenceProvider(fastaURL, indexURL, compressedIndexURL, fet
9742
9742
  const faiURL = indexURL !== null && indexURL !== void 0 ? indexURL : fastaURL + '.fai';
9743
9743
  return async (locus, signal) => {
9744
9744
  var _a;
9745
- const reader = getReader$2(fastaURL, faiURL, compressedIndexURL, fetchImpl);
9745
+ const reader = getReader$3(fastaURL, faiURL, compressedIndexURL, fetchImpl);
9746
9746
  try {
9747
9747
  const sequence = await reader.getSequence(locus.chr, locus.start, locus.end, { signal });
9748
9748
  return sequence !== null && sequence !== void 0 ? sequence : '';
9749
9749
  }
9750
9750
  catch (err) {
9751
9751
  if (err instanceof Error && (err.name === 'AbortError' || ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('aborted')))) {
9752
- readerCache$3.delete(fastaURL);
9752
+ readerCache$4.delete(fastaURL);
9753
9753
  }
9754
9754
  throw err;
9755
9755
  }
@@ -9877,15 +9877,49 @@ function createGenomeSync(config) {
9877
9877
  return new GenomeImpl(id, config.chromSizes, { name, sequence, cytobands: config.cytobands, nameSet: config.nameSet });
9878
9878
  }
9879
9879
  // ─── Sequence resolution ────��─────────────────────────────────���──────────────
9880
+ /**
9881
+ * Wrap a primary SequenceProvider with a fallback.
9882
+ *
9883
+ * On each request, tries `primary` first. If it throws (and the signal was
9884
+ * not aborted), tries `fallback`. If both fail, the primary error is thrown
9885
+ * so the caller sees the error from their configured source.
9886
+ *
9887
+ * The caching layer (createCachedSequence) wraps the combined provider,
9888
+ * so successful fallback results are cached normally — subsequent requests
9889
+ * for the same region won't re-attempt the primary.
9890
+ */
9891
+ function createFallbackProvider(primary, fallback) {
9892
+ return async (locus, signal) => {
9893
+ try {
9894
+ return await primary(locus, signal);
9895
+ }
9896
+ catch (primaryErr) {
9897
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted)
9898
+ throw primaryErr;
9899
+ console.warn('[loom] Primary sequence source failed, trying fallback:', primaryErr.message);
9900
+ try {
9901
+ return await fallback(locus, signal);
9902
+ }
9903
+ catch (_a) {
9904
+ // Both failed — throw the primary error (more relevant to the user's config)
9905
+ throw primaryErr;
9906
+ }
9907
+ }
9908
+ };
9909
+ }
9880
9910
  /**
9881
9911
  * Resolve a sequence provider from config.
9882
9912
  *
9883
9913
  * Exactly one sequence source must be set. Throws if multiple are provided.
9884
9914
  * Sources: sequenceProvider, twoBitURL, fastaURL, ucscGenome.
9885
9915
  * When none is set, falls back to UCSC API via `id` if available.
9916
+ *
9917
+ * When twoBitURL is used and a genome ID is available, the UCSC REST API is
9918
+ * automatically added as a fallback — if the 2-bit server is unreachable,
9919
+ * the API is tried instead.
9886
9920
  */
9887
9921
  function resolveSequenceProvider(config) {
9888
- var _a;
9922
+ var _a, _b;
9889
9923
  const sources = [
9890
9924
  config.sequenceProvider && 'sequenceProvider',
9891
9925
  config.twoBitURL && 'twoBitURL',
@@ -9900,13 +9934,20 @@ function resolveSequenceProvider(config) {
9900
9934
  return config.sequenceProvider;
9901
9935
  }
9902
9936
  if (config.twoBitURL) {
9903
- return createCachedSequence(createTwoBitSequenceProvider(config.twoBitURL, config.fetchImpl));
9937
+ const primary = createTwoBitSequenceProvider(config.twoBitURL, config.fetchImpl);
9938
+ // Add UCSC REST API fallback when a genome ID is known
9939
+ const genomeId = (_a = config.ucscGenome) !== null && _a !== void 0 ? _a : config.id;
9940
+ if (genomeId) {
9941
+ const fallback = (locus, signal) => fetchSequence(locus, { genome: genomeId }, signal);
9942
+ return createCachedSequence(createFallbackProvider(primary, fallback));
9943
+ }
9944
+ return createCachedSequence(primary);
9904
9945
  }
9905
9946
  if (config.fastaURL) {
9906
9947
  return createCachedSequence(createFastaSequenceProvider(config.fastaURL, config.indexURL, config.compressedIndexURL, config.fetchImpl));
9907
9948
  }
9908
9949
  // UCSC API — explicit ucscGenome, or implicit via id
9909
- const ucscGenome = (_a = config.ucscGenome) !== null && _a !== void 0 ? _a : config.id;
9950
+ const ucscGenome = (_b = config.ucscGenome) !== null && _b !== void 0 ? _b : config.id;
9910
9951
  if (ucscGenome) {
9911
9952
  return createCachedSequence((locus, signal) => fetchSequence(locus, { genome: ucscGenome }, signal));
9912
9953
  }
@@ -9914,13 +9955,15 @@ function resolveSequenceProvider(config) {
9914
9955
  }
9915
9956
  // ─── Pre-built singletons ��─────────��─────────────────────────────────────────
9916
9957
  /**
9917
- * Pre-built hg38 genome with UCSC sequence API.
9958
+ * Pre-built hg38 genome with 2-bit sequence (UCSC API fallback).
9918
9959
  *
9919
9960
  * This is the default genome used by HeadlessGenomeBrowser when no genome
9920
- * is specified. Equivalent to igv.js's default behavior of loading hg38.
9961
+ * is specified. Matches igv.js's default behavior: 2-bit file as primary
9962
+ * sequence source, with UCSC REST API as automatic fallback for resilience.
9921
9963
  */
9922
9964
  const hg38Genome = createGenomeSync({
9923
9965
  id: 'hg38',
9966
+ twoBitURL: 'https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.2bit',
9924
9967
  chromSizes: hg38ChromSizes,
9925
9968
  });
9926
9969
 
@@ -10011,6 +10054,12 @@ const binaryFormats = new Set(['bigwig', 'bw', 'bigbed', 'bb', 'biginteract', 'b
10011
10054
  function isBinaryFormat(format) {
10012
10055
  return binaryFormats.has(format.toLowerCase());
10013
10056
  }
10057
+ /** BigBed-specific formats (annotation data, not quantitative). */
10058
+ const bigBedFormats = new Set(['bigbed', 'bb', 'biginteract', 'biggenepred', 'bignarrowpeak']);
10059
+ /** Check if a format string represents a BigBed (annotation) format vs BigWig (quantitative). */
10060
+ function isBigBedFormat(format) {
10061
+ return bigBedFormats.has(format.toLowerCase());
10062
+ }
10014
10063
  /**
10015
10064
  * Check if a URL is likely tabix-indexed.
10016
10065
  *
@@ -10050,7 +10099,7 @@ function inferIndexURL(url) {
10050
10099
  * Layer 1 (Data): no DOM, no canvas.
10051
10100
  */
10052
10101
  /** Known data source config types. */
10053
- const KNOWN_DS_TYPES = new Set(['bigwig', 'gtx', 'ucsc', 'text', 'memory']);
10102
+ const KNOWN_DS_TYPES = new Set(['bigwig', 'bigbed', 'gtx', 'ucsc', 'text', 'memory']);
10054
10103
  /** Check whether a data source config type is one of the known concrete types. */
10055
10104
  function isKnownDataSourceType(type) {
10056
10105
  return KNOWN_DS_TYPES.has(type);
@@ -10084,7 +10133,16 @@ function resolveDataSourceConfig(config) {
10084
10133
  auth.headers = config.headers;
10085
10134
  if (config.withCredentials)
10086
10135
  auth.withCredentials = config.withCredentials;
10087
- // BigWig / BigBed binary formats
10136
+ // BigBed binary formats (annotation data)
10137
+ if (format && isBigBedFormat(format)) {
10138
+ log.warn(`Unknown data source type "${type}", inferred "bigbed" from URL: ${url}`);
10139
+ return {
10140
+ type: 'bigbed',
10141
+ url,
10142
+ ...auth,
10143
+ };
10144
+ }
10145
+ // BigWig / other binary formats (quantitative data)
10088
10146
  if (format && (format === 'bigwig' || format === 'bw' || isBinaryFormat(format))) {
10089
10147
  log.warn(`Unknown data source type "${type}", inferred "bigwig" from URL: ${url}`);
10090
10148
  return {
@@ -13384,14 +13442,14 @@ class BinaryParser {
13384
13442
  // ─── Internal helpers ─────────────────────────────────────────────────────────
13385
13443
  // Cache BigWig instances by URL to reuse parsed headers and trees.
13386
13444
  // Auth-aware readers share the cache key (URL) — the fetchImpl is bound at creation.
13387
- const readerCache$2 = new Map();
13445
+ const readerCache$3 = new Map();
13388
13446
  function getGmodReader(url, fetchImpl) {
13389
13447
  const resolvedUrl = normalizeGoogleURL(url);
13390
- let reader = readerCache$2.get(resolvedUrl);
13448
+ let reader = readerCache$3.get(resolvedUrl);
13391
13449
  if (!reader) {
13392
13450
  const fileOpts = fetchImpl ? { fetch: fetchImpl } : undefined;
13393
13451
  reader = new BigWig({ filehandle: new RemoteFile(resolvedUrl, fileOpts) });
13394
- readerCache$2.set(resolvedUrl, reader);
13452
+ readerCache$3.set(resolvedUrl, reader);
13395
13453
  }
13396
13454
  return reader;
13397
13455
  }
@@ -13512,7 +13570,7 @@ async function fetchBigWigFeatures(url, locus, options = {}) {
13512
13570
  // Evict the cached reader so the next fetch gets a fresh one.
13513
13571
  // @gmod/bbi caches the header promise; an aborted parse poisons it permanently.
13514
13572
  if (err instanceof Error && (err.name === 'AbortError' || ((_b = err.message) === null || _b === void 0 ? void 0 : _b.includes('aborted')))) {
13515
- readerCache$2.delete(url);
13573
+ readerCache$3.delete(url);
13516
13574
  }
13517
13575
  throw err;
13518
13576
  }
@@ -13640,6 +13698,102 @@ class BigWigDataSource {
13640
13698
  }
13641
13699
  }
13642
13700
 
13701
+ /**
13702
+ * BigBed data source — fetches BED features from BigBed binary files.
13703
+ *
13704
+ * Uses @gmod/bbi's BigBed class for binary parsing and range queries.
13705
+ * Returns BedFeature[] suitable for annotation track rendering.
13706
+ *
13707
+ * Mirrors igv.js BWSource behavior for bigbed format:
13708
+ * - Features are decoded from BED rest fields via decodeBed()
13709
+ * - No zoom-level summarization (unlike BigWig)
13710
+ * - Feature density estimation from header dataCount
13711
+ *
13712
+ * Layer 1 (Data + Layout): no DOM.
13713
+ */
13714
+ // Cache BigBed instances by URL to reuse parsed headers and index trees.
13715
+ const readerCache$2 = new Map();
13716
+ function getReader$2(url, fetchImpl) {
13717
+ const resolvedUrl = normalizeGoogleURL(url);
13718
+ let reader = readerCache$2.get(resolvedUrl);
13719
+ if (!reader) {
13720
+ const fileOpts = fetchImpl ? { fetch: fetchImpl } : undefined;
13721
+ reader = new BigBed({ filehandle: new RemoteFile(resolvedUrl, fileOpts) });
13722
+ readerCache$2.set(resolvedUrl, reader);
13723
+ }
13724
+ return reader;
13725
+ }
13726
+ class BigBedDataSource {
13727
+ constructor(url, fetchImpl) {
13728
+ this.url = url;
13729
+ this.fetchImpl = fetchImpl;
13730
+ this.bb = getReader$2(url, fetchImpl);
13731
+ }
13732
+ /** Set a chromosome name resolver for alias resolution (e.g., "1" → "chr1"). */
13733
+ setChromNameResolver(resolver) {
13734
+ this._resolveChromName = resolver;
13735
+ }
13736
+ async fetch(locus, _bpPerPixel, signal) {
13737
+ var _a;
13738
+ const chr = this._resolveChromName
13739
+ ? this._resolveChromName(locus.chr)
13740
+ : locus.chr;
13741
+ try {
13742
+ const rawFeatures = await this.bb.getFeatures(chr, locus.start, locus.end, { signal });
13743
+ const result = [];
13744
+ for (const f of rawFeatures) {
13745
+ const parsed = parseBigBedFeature(chr, f.start, f.end, f.rest);
13746
+ if (parsed)
13747
+ result.push(parsed);
13748
+ }
13749
+ return result;
13750
+ }
13751
+ catch (err) {
13752
+ // Evict poisoned reader on abort (same pattern as BigWig)
13753
+ if (err instanceof Error && (err.name === 'AbortError' || ((_a = err.message) === null || _a === void 0 ? void 0 : _a.includes('aborted')))) {
13754
+ readerCache$2.delete(normalizeGoogleURL(this.url));
13755
+ }
13756
+ throw err;
13757
+ }
13758
+ }
13759
+ /**
13760
+ * Search for a feature by name via BigBed extra index.
13761
+ * Returns matching features, or empty array if not found or not indexed.
13762
+ */
13763
+ async search(name) {
13764
+ const header = await this.bb.getHeader();
13765
+ const results = await this.bb.searchExtraIndex(name);
13766
+ if (results.length === 0)
13767
+ return [];
13768
+ const refsByNumber = header.refsByNumber;
13769
+ const features = [];
13770
+ for (const r of results) {
13771
+ const chromId = r.chromId;
13772
+ if (chromId == null || !(refsByNumber === null || refsByNumber === void 0 ? void 0 : refsByNumber[chromId]))
13773
+ continue;
13774
+ const chr = refsByNumber[chromId].name;
13775
+ const parsed = parseBigBedFeature(chr, r.start, r.end, r.rest);
13776
+ if (parsed)
13777
+ features.push(parsed);
13778
+ }
13779
+ return features;
13780
+ }
13781
+ dispose() {
13782
+ readerCache$2.delete(normalizeGoogleURL(this.url));
13783
+ }
13784
+ }
13785
+ /**
13786
+ * Parse a BigBed feature's rest field into a BedFeature.
13787
+ * Reconstructs BED tokens from chr/start/end + tab-delimited rest, then uses decodeBed().
13788
+ */
13789
+ function parseBigBedFeature(chr, start, end, rest) {
13790
+ const tokens = [chr, String(start), String(end)];
13791
+ if (rest) {
13792
+ tokens.push(...rest.split('\t'));
13793
+ }
13794
+ return decodeBed(tokens);
13795
+ }
13796
+
13643
13797
  /**
13644
13798
  * HTTP range request reader using native fetch.
13645
13799
  * Replaces igvxhr + BufferedReader for BigWig file reading.
@@ -20551,6 +20705,8 @@ function createDataSource(config) {
20551
20705
  switch (config.type) {
20552
20706
  case 'bigwig':
20553
20707
  return new BigWigDataSource(config.url, config.windowFunction);
20708
+ case 'bigbed':
20709
+ return new BigBedDataSource(config.url);
20554
20710
  case 'gtx':
20555
20711
  return new GtxDataSource(config.url, config.experimentId, config.windowFunction);
20556
20712
  case 'ucsc':
@@ -20832,6 +20988,11 @@ registerTypeAlias('broadpeak', 'annotation');
20832
20988
  registerTypeAlias('genepred', 'annotation');
20833
20989
  registerTypeAlias('genepredext', 'annotation');
20834
20990
  registerTypeAlias('refflat', 'annotation');
20991
+ // BigBed format → annotation track type aliases
20992
+ registerTypeAlias('bigbed', 'annotation');
20993
+ registerTypeAlias('bedtype', 'annotation');
20994
+ registerTypeAlias('biggenepred', 'annotation');
20995
+ registerTypeAlias('bignarrowpeak', 'annotation');
20835
20996
  // Merged/overlay aliases
20836
20997
  registerTypeAlias('overlay', 'merged');
20837
20998
  // Interaction format aliases
@@ -21984,10 +22145,12 @@ function isZoomAware(track) {
21984
22145
  * tracks point to the same URL/region.
21985
22146
  */
21986
22147
  function dataSourceCacheKey(config) {
21987
- var _a, _b, _c, _d, _e;
22148
+ var _a, _b, _c, _d, _e, _f;
21988
22149
  switch (config.type) {
21989
22150
  case 'bigwig':
21990
22151
  return `bigwig:${config.url}:${(_a = config.windowFunction) !== null && _a !== void 0 ? _a : 'mean'}`;
22152
+ case 'bigbed':
22153
+ return `bigbed:${config.url}`;
21991
22154
  case 'gtx':
21992
22155
  return `gtx:${config.url}:${config.experimentId}`;
21993
22156
  case 'ucsc':
@@ -21995,13 +22158,9 @@ function dataSourceCacheKey(config) {
21995
22158
  case 'text':
21996
22159
  return `text:${config.url}:${(_d = config.format) !== null && _d !== void 0 ? _d : ''}:${(_e = config.indexURL) !== null && _e !== void 0 ? _e : ''}`;
21997
22160
  case 'memory':
21998
- // Each memory data source is unique — no deduplication.
21999
22161
  return `memory:${Math.random()}`;
22000
- default: {
22001
- // Unknown type resolve via URL inference, then compute key from resolved config
22002
- const resolved = resolveDataSourceConfig(config);
22003
- return dataSourceCacheKey(resolved);
22004
- }
22162
+ default:
22163
+ return `unknown:${config.type}:${(_f = config.url) !== null && _f !== void 0 ? _f : Math.random()}`;
22005
22164
  }
22006
22165
  }
22007
22166
 
@@ -22670,6 +22829,9 @@ function wireDataSource(ds, ctx) {
22670
22829
  if (ds instanceof BigWigDataSource || ds instanceof GtxDataSource) {
22671
22830
  ds.setChromNameResolver(alias => ctx.genome.getChromosomeName(alias));
22672
22831
  }
22832
+ else if (ds instanceof BigBedDataSource) {
22833
+ ds.setChromNameResolver(alias => ctx.genome.getChromosomeName(alias));
22834
+ }
22673
22835
  else if (ds instanceof TextFeatureSource) {
22674
22836
  ds.setChromNameResolver(alias => ctx.genome.getChromosomeName(alias));
22675
22837
  if (ctx.cumulativeOffsets) {
@@ -22857,6 +23019,30 @@ function createBedTrack(ctx, url, options) {
22857
23019
  searchableFields: options === null || options === void 0 ? void 0 : options.searchableFields,
22858
23020
  };
22859
23021
  }
23022
+ function createBigBedTrack(ctx, url, options) {
23023
+ var _a;
23024
+ const { canvas } = ctx.canvasProvider.createCanvas(0, 0);
23025
+ const track = new AnnotationTrackCanvas(canvas, {
23026
+ locus: ctx.locus,
23027
+ features: [],
23028
+ config: options === null || options === void 0 ? void 0 : options.config,
23029
+ theme: ctx.theme,
23030
+ canvasProvider: ctx.canvasProvider,
23031
+ name: options === null || options === void 0 ? void 0 : options.name,
23032
+ });
23033
+ const dataSourceConfig = { type: 'bigbed', url, ...options === null || options === void 0 ? void 0 : options.auth };
23034
+ const fetchImpl = resolveTrackFetchImpl(ctx, options);
23035
+ const { dataSource } = getOrCreateDataSource(dataSourceConfig, () => new BigBedDataSource(url, fetchImpl), ctx);
23036
+ return {
23037
+ track,
23038
+ dataSource,
23039
+ dataSourceConfig,
23040
+ maxTrackHeight: options === null || options === void 0 ? void 0 : options.maxTrackHeight,
23041
+ metadata: options === null || options === void 0 ? void 0 : options.metadata,
23042
+ searchable: (_a = options === null || options === void 0 ? void 0 : options.searchable) !== null && _a !== void 0 ? _a : true,
23043
+ searchableFields: options === null || options === void 0 ? void 0 : options.searchableFields,
23044
+ };
23045
+ }
22860
23046
  function createInteractionTrack(ctx, url, options) {
22861
23047
  var _a;
22862
23048
  const { canvas } = ctx.canvasProvider.createCanvas(0, 0);
@@ -23206,10 +23392,10 @@ class HeadlessGenomeBrowser {
23206
23392
  }
23207
23393
  // ─── Track lifecycle ─────────────────────────────────────────────────────
23208
23394
  /** Add a track with an optional data source for automatic data management. */
23209
- addTrack(track, dataSource, dataSourceConfig, maxTrackHeight, order) {
23395
+ addTrack(track, dataSource, dataSourceConfig, maxTrackHeight, order, id) {
23210
23396
  var _a;
23211
23397
  this._snapshotForUndo('track-add');
23212
- const id = generateTrackId(track.type);
23398
+ id !== null && id !== void 0 ? id : (id = generateTrackId(track.type));
23213
23399
  const mt = {
23214
23400
  id,
23215
23401
  track,
@@ -24086,11 +24272,9 @@ class HeadlessGenomeBrowser {
24086
24272
  if (!this.dataSourceWorkerProvider && dataSource) {
24087
24273
  wireDataSource(dataSource, ctx);
24088
24274
  }
24089
- this.addTrack(created.track, dataSource !== null && dataSource !== void 0 ? dataSource : undefined, (_a = created.dataSourceConfig) !== null && _a !== void 0 ? _a : undefined, undefined, (_b = created.order) !== null && _b !== void 0 ? _b : undefined);
24275
+ this.addTrack(created.track, dataSource !== null && dataSource !== void 0 ? dataSource : undefined, (_a = created.dataSourceConfig) !== null && _a !== void 0 ? _a : undefined, undefined, (_b = created.order) !== null && _b !== void 0 ? _b : undefined, trackConfig.id);
24090
24276
  // Restore bookkeeping fields for round-trip serialization
24091
24277
  const mt = this.findMT(created.track);
24092
- if (trackConfig.id)
24093
- mt.id = trackConfig.id;
24094
24278
  if (created.name)
24095
24279
  mt.name = created.name;
24096
24280
  if (trackConfig.metadata)
@@ -24176,10 +24360,8 @@ class HeadlessGenomeBrowser {
24176
24360
  wireDataSource(dataSource, ctx);
24177
24361
  }
24178
24362
  }
24179
- this.addTrack(created.track, dataSource !== null && dataSource !== void 0 ? dataSource : undefined, dataSourceConfig !== null && dataSourceConfig !== void 0 ? dataSourceConfig : undefined, undefined, (_a = created.order) !== null && _a !== void 0 ? _a : undefined);
24363
+ this.addTrack(created.track, dataSource !== null && dataSource !== void 0 ? dataSource : undefined, dataSourceConfig !== null && dataSourceConfig !== void 0 ? dataSourceConfig : undefined, undefined, (_a = created.order) !== null && _a !== void 0 ? _a : undefined, trackConfig.id);
24180
24364
  const mt = this.findMT(created.track);
24181
- if (trackConfig.id)
24182
- mt.id = trackConfig.id;
24183
24365
  if (created.name)
24184
24366
  mt.name = created.name;
24185
24367
  if (trackConfig.metadata)
@@ -24238,6 +24420,10 @@ class HeadlessGenomeBrowser {
24238
24420
  addBedTrack(url, options) {
24239
24421
  return this.registerFactory(createBedTrack(this.factoryContext(), url, options));
24240
24422
  }
24423
+ /** Add a BigBed annotation track from a URL. */
24424
+ addBigBedTrack(url, options) {
24425
+ return this.registerFactory(createBigBedTrack(this.factoryContext(), url, options));
24426
+ }
24241
24427
  /** Add an interaction (arc/BEDPE) track from a URL. */
24242
24428
  addInteractionTrack(url, options) {
24243
24429
  return this.registerFactory(createInteractionTrack(this.factoryContext(), url, options));
@@ -28436,23 +28622,26 @@ const LoomBrowser = forwardRef(function LoomBrowser(props, ref) {
28436
28622
  options.genome = genome;
28437
28623
  const b = new GenomeBrowser(container, options);
28438
28624
  browserRef.current = b;
28439
- // Load session tracks if provided
28625
+ // Load session tracks if provided — a session is a complete state
28626
+ // snapshot, so skip default tracks to avoid duplicates.
28440
28627
  if (session) {
28441
28628
  b.loadSession(session);
28442
28629
  }
28630
+ else {
28631
+ // Convenience default tracks (default: true)
28632
+ if (ruler !== false)
28633
+ b.addRuler();
28634
+ if (genes !== false)
28635
+ b.addGeneTrack();
28636
+ if (sequence !== false)
28637
+ b.addSequenceTrack();
28638
+ }
28443
28639
  // Add config-driven tracks
28444
28640
  if (trackConfigs) {
28445
28641
  for (const tc of trackConfigs) {
28446
28642
  b.addTrackFromConfig(tc);
28447
28643
  }
28448
28644
  }
28449
- // Convenience default tracks (default: true)
28450
- if (ruler !== false)
28451
- b.addRuler();
28452
- if (genes !== false)
28453
- b.addGeneTrack();
28454
- if (sequence !== false)
28455
- b.addSequenceTrack();
28456
28645
  // Forward locus changes to onLocusChange / onFrameChange (for controlled mode)
28457
28646
  b.on(BrowserEvent.LocusChange, (event) => {
28458
28647
  var _a, _b;