loom-browser 0.0.7 → 0.0.8

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.
package/dist/loom.esm.js CHANGED
@@ -5045,8 +5045,11 @@ class WebWorkerPool {
5045
5045
  this.readyPromises = [];
5046
5046
  for (let i = 0; i < count; i++) {
5047
5047
  const worker = createWorker();
5048
- // Ready handshake
5049
- const readyPromise = new Promise((resolveReady) => {
5048
+ // Ready handshake — rejects on worker error so awaiting fetches
5049
+ // don't hang forever if the worker script fails to load.
5050
+ let rejectReady;
5051
+ const readyPromise = new Promise((resolveReady, rej) => {
5052
+ rejectReady = rej;
5050
5053
  const onReady = (e) => {
5051
5054
  if (e.data.type === 'ready') {
5052
5055
  resolveReady();
@@ -5054,6 +5057,8 @@ class WebWorkerPool {
5054
5057
  };
5055
5058
  worker.addEventListener('message', onReady, { once: true });
5056
5059
  });
5060
+ // Prevent unhandled rejection warnings if worker fails before any fetch
5061
+ readyPromise.catch(() => { });
5057
5062
  this.readyPromises.push(readyPromise);
5058
5063
  worker.onmessage = (e) => {
5059
5064
  const msg = e.data;
@@ -5093,6 +5098,7 @@ class WebWorkerPool {
5093
5098
  };
5094
5099
  worker.onerror = (e) => {
5095
5100
  const error = new Error(`Worker error: ${e.message}`);
5101
+ rejectReady(error);
5096
5102
  for (const { reject } of this.pending.values())
5097
5103
  reject(error);
5098
5104
  this.pending.clear();
@@ -5357,10 +5363,12 @@ class BaseTrackCanvas {
5357
5363
  ctx.fillRect(0, 0, width, height);
5358
5364
  if (this._error) {
5359
5365
  this.renderError(ctx, width, height);
5366
+ this.renderLabelOverlay(ctx, width, height);
5360
5367
  return;
5361
5368
  }
5362
5369
  if (this._zoomedOut) {
5363
5370
  this.renderZoomInNotice(ctx, width, height);
5371
+ this.renderLabelOverlay(ctx, width, height);
5364
5372
  return;
5365
5373
  }
5366
5374
  const bpPerPixel = (this._locus.end - this._locus.start) / width;
@@ -5391,6 +5399,14 @@ class BaseTrackCanvas {
5391
5399
  ctx.textBaseline = 'middle';
5392
5400
  ctx.fillText('Zoom in to see features', width / 2, height / 2);
5393
5401
  }
5402
+ /**
5403
+ * Render the track name label overlay. Called after error/zoom-in notices
5404
+ * so labels remain visible even when the track can't render data.
5405
+ * No-op by default — subclasses with name labels should override.
5406
+ */
5407
+ renderLabelOverlay(_ctx, _width, _height) {
5408
+ // No-op — subclasses override
5409
+ }
5394
5410
  /**
5395
5411
  * Render this track onto an arbitrary context (e.g. Canvas2SVG for SVG export).
5396
5412
  * Skips DPR scaling and canvas-element lifecycle — draws directly at the given
@@ -5405,10 +5421,12 @@ class BaseTrackCanvas {
5405
5421
  ctx.fillRect(0, 0, width, height);
5406
5422
  if (this._error) {
5407
5423
  this.renderError(ctx, width, height);
5424
+ this.renderLabelOverlay(ctx, width, height);
5408
5425
  return;
5409
5426
  }
5410
5427
  if (this._zoomedOut) {
5411
5428
  this.renderZoomInNotice(ctx, width, height);
5429
+ this.renderLabelOverlay(ctx, width, height);
5412
5430
  return;
5413
5431
  }
5414
5432
  const bpPerPixel = (this._locus.end - this._locus.start) / width;
@@ -6129,6 +6147,16 @@ class AnnotationTrackCanvas extends BaseTrackCanvas {
6129
6147
  getBackground() {
6130
6148
  return this.background;
6131
6149
  }
6150
+ renderLabelOverlay(ctx, _width, height) {
6151
+ var _a;
6152
+ if (this._name) {
6153
+ renderTrackNameLabel(ctx, {
6154
+ name: this._name,
6155
+ background: this.background,
6156
+ labelColor: (_a = this.config.labelColor) !== null && _a !== void 0 ? _a : '#333',
6157
+ }, height);
6158
+ }
6159
+ }
6132
6160
  doRender(ctx, _width, _height, rc) {
6133
6161
  var _a;
6134
6162
  // Ensure label background matches track background so clearRect doesn't
@@ -7219,15 +7247,18 @@ class WigTrackCanvas extends BaseTrackCanvas {
7219
7247
  getBackground() {
7220
7248
  return this.config.background;
7221
7249
  }
7250
+ renderLabelOverlay(ctx, _width, height) {
7251
+ if (this._name) {
7252
+ renderTrackNameLabel(ctx, {
7253
+ name: this._name,
7254
+ background: this.config.background,
7255
+ labelColor: this.config.labelColor,
7256
+ }, height);
7257
+ }
7258
+ }
7222
7259
  doRender(ctx, _width, height, rc) {
7223
7260
  if (this.features.length === 0) {
7224
- if (this._name) {
7225
- renderTrackNameLabel(ctx, {
7226
- name: this._name,
7227
- background: this.config.background,
7228
- labelColor: this.config.labelColor,
7229
- }, height);
7230
- }
7261
+ this.renderLabelOverlay(ctx, _width, height);
7231
7262
  return;
7232
7263
  }
7233
7264
  // Apply value scaling (normalize then scaleFactor), matching igv.js getFeatures() order.
@@ -9835,6 +9866,15 @@ class InteractionTrackCanvas extends BaseTrackCanvas {
9835
9866
  getBackground() {
9836
9867
  return this.config.background;
9837
9868
  }
9869
+ renderLabelOverlay(ctx, _width, height) {
9870
+ if (this._name) {
9871
+ renderTrackNameLabel(ctx, {
9872
+ name: this._name,
9873
+ background: this._config.background,
9874
+ labelColor: '#333',
9875
+ }, height);
9876
+ }
9877
+ }
9838
9878
  doRender(ctx, _width, _height, rc) {
9839
9879
  renderInteractionTrack(ctx, this.features, this._config, rc, this._name ? {
9840
9880
  name: this._name,
@@ -10872,6 +10912,19 @@ function isDataSourceWorkerProvider(obj) {
10872
10912
  && typeof o.fetch === 'function'
10873
10913
  && typeof o.destroy === 'function';
10874
10914
  }
10915
+ /** Check if an error is an AbortError (from fetch abort, signal abort, or worker abort). */
10916
+ function isAbortError(err) {
10917
+ if (err instanceof DOMException && err.name === 'AbortError')
10918
+ return true;
10919
+ if (err instanceof Error && err.name === 'AbortError')
10920
+ return true;
10921
+ // Worker-relayed abort errors may arrive as plain Error with the browser's
10922
+ // abort message (e.g., "signal is aborted without reason") because the
10923
+ // worker-side DOMException check can miss non-DOMException abort errors.
10924
+ if (err instanceof Error && err.message.includes('aborted'))
10925
+ return true;
10926
+ return false;
10927
+ }
10875
10928
  const BrowserEvent = {
10876
10929
  LocusChange: 'locuschange',
10877
10930
  TrackAdded: 'trackadded',
@@ -10932,6 +10985,8 @@ class HeadlessGenomeBrowser {
10932
10985
  this.inflightFetches = new Map();
10933
10986
  /** Timer for debouncing data loads during rapid zoom/pan. */
10934
10987
  this.loadDebounceTimer = null;
10988
+ /** Set to true by dispose() to suppress post-dispose promise handlers. */
10989
+ this._disposed = false;
10935
10990
  /** When true, sortTracks() is a no-op. Used for batch track additions (e.g. loadSession). */
10936
10991
  this._deferSort = false;
10937
10992
  this.events = new EventEmitter();
@@ -11296,6 +11351,17 @@ class HeadlessGenomeBrowser {
11296
11351
  }
11297
11352
  return undefined;
11298
11353
  }
11354
+ /** Remove a specific ROI set by instance. Returns true if found and removed. */
11355
+ removeROISet(set) {
11356
+ const idx = this.roiSets.indexOf(set);
11357
+ if (idx < 0)
11358
+ return false;
11359
+ this.roiSets.splice(idx, 1);
11360
+ for (const roi of set.features) {
11361
+ this.events.emit(BrowserEvent.ROIRemoved, { roi, set });
11362
+ }
11363
+ return true;
11364
+ }
11299
11365
  /** Remove all ROI sets. */
11300
11366
  clearROIs() {
11301
11367
  this.roiSets = [];
@@ -11837,6 +11903,11 @@ class HeadlessGenomeBrowser {
11837
11903
  }
11838
11904
  /** Clean up event listeners, abort in-flight requests, clear tracks and ROIs. */
11839
11905
  dispose() {
11906
+ this._disposed = true;
11907
+ if (this.loadDebounceTimer !== null) {
11908
+ clearTimeout(this.loadDebounceTimer);
11909
+ this.loadDebounceTimer = null;
11910
+ }
11840
11911
  this.events.removeAllListeners();
11841
11912
  if (this.popupProvider)
11842
11913
  this.popupProvider.dispose();
@@ -11980,12 +12051,16 @@ class HeadlessGenomeBrowser {
11980
12051
  this.events.emit(BrowserEvent.DataLoaded, { track: mt.track });
11981
12052
  })
11982
12053
  .catch(err => {
11983
- // AbortErrors propagate from the source track's abort — ignore them
11984
- if (err instanceof DOMException && err.name === 'AbortError')
12054
+ if (this._disposed || isAbortError(err))
11985
12055
  return;
11986
12056
  const error = err instanceof Error ? err : new Error(String(err));
11987
- console.error('[loom] Data fetch error (dedup):', error);
11988
- mt.track.setError(error);
12057
+ if (mt.cache) {
12058
+ console.warn('[loom] Data fetch failed (dedup), showing cached data:', error.message);
12059
+ }
12060
+ else {
12061
+ console.error('[loom] Data fetch error (dedup):', error);
12062
+ mt.track.setError(error);
12063
+ }
11989
12064
  this.events.emit(BrowserEvent.DataError, { track: mt.track, error });
11990
12065
  });
11991
12066
  return;
@@ -12012,12 +12087,20 @@ class HeadlessGenomeBrowser {
12012
12087
  this.events.emit(BrowserEvent.DataLoaded, { track: mt.track });
12013
12088
  })
12014
12089
  .catch(err => {
12015
- if (!controller.signal.aborted) {
12016
- const error = err instanceof Error ? err : new Error(String(err));
12090
+ if (this._disposed || controller.signal.aborted || isAbortError(err))
12091
+ return;
12092
+ const error = err instanceof Error ? err : new Error(String(err));
12093
+ // If we have cached data, keep showing it (stale) rather than
12094
+ // replacing it with an error message. Only show error if there's
12095
+ // nothing to display at all.
12096
+ if (mt.cache) {
12097
+ console.warn('[loom] Data fetch failed, showing cached data:', error.message);
12098
+ }
12099
+ else {
12017
12100
  console.error('[loom] Data fetch error:', error);
12018
12101
  mt.track.setError(error);
12019
- this.events.emit(BrowserEvent.DataError, { track: mt.track, error });
12020
12102
  }
12103
+ this.events.emit(BrowserEvent.DataError, { track: mt.track, error });
12021
12104
  })
12022
12105
  .finally(() => {
12023
12106
  var _a;
@@ -12890,8 +12973,16 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
12890
12973
  document.addEventListener("mousedown", this.handleDocMouseDown);
12891
12974
  }
12892
12975
  }
12893
- // Re-render tracks when container resizes (e.g. first layout in Shadow DOM)
12894
- this.resizeObserver = new ResizeObserver(() => this.render());
12976
+ // Re-render tracks when container resizes (e.g. first layout in Shadow DOM).
12977
+ // Also trigger data loads if width went from 0 → non-zero (common in React
12978
+ // flex layouts where the container hasn't laid out when GenomeBrowser is created).
12979
+ this.resizeObserver = new ResizeObserver(() => {
12980
+ const prevWidth = this._viewportWidth;
12981
+ this.render(); // updates _viewportWidth from container.clientWidth
12982
+ if (prevWidth === 0 && this._viewportWidth > 0) {
12983
+ this.loadAllTracksIfNeeded();
12984
+ }
12985
+ });
12895
12986
  this.resizeObserver.observe(container);
12896
12987
  // Axis content is updated in two places:
12897
12988
  // 1. After super.render() in render() — handles resize, pan, initial load