loom-browser 0.0.7 → 0.0.9

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