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-react.esm.js +181 -33
- package/dist/loom-react.esm.min.js +1 -1
- package/dist/loom-react.esm.min.js.map +1 -1
- package/dist/loom-worker.js +6 -1
- package/dist/loom-worker.min.js +1 -1
- package/dist/loom-worker.min.js.map +1 -1
- package/dist/loom.esm.js +109 -18
- package/dist/loom.esm.min.js +1 -1
- package/dist/loom.esm.min.js.map +1 -1
- package/dist/loom.js +109 -18
- package/dist/loom.min.js +1 -1
- package/dist/loom.min.js.map +1 -1
- package/dist/tsconfig.src.tsbuildinfo +1 -1
- package/dist/types/headlessGenomeBrowser.d.ts +6 -4
- package/dist/types/react/LoomBrowser.d.ts +1 -0
- package/dist/types/react/ROISet.d.ts +14 -0
- package/dist/types/react/hooks/index.d.ts +1 -0
- package/dist/types/react/hooks/useTrackManager.d.ts +13 -1
- package/dist/types/react/index.d.ts +3 -0
- package/dist/types/react/tracks/BedTrack.d.ts +10 -5
- package/dist/types/react/tracks/GeneTrack.d.ts +7 -2
- package/dist/types/react/tracks/GtxTrack.d.ts +7 -2
- package/dist/types/react/tracks/InteractionTrack.d.ts +6 -1
- package/dist/types/react/tracks/WigTrack.d.ts +6 -1
- package/dist/types/tracks/annotation/annotationTrackCanvas.d.ts +1 -0
- package/dist/types/tracks/baseTrackCanvas.d.ts +6 -0
- package/dist/types/tracks/interaction/interactionTrackCanvas.d.ts +1 -0
- package/dist/types/tracks/wig/wigTrackCanvas.d.ts +1 -0
- package/dist/types/types.d.ts +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11988
|
-
|
|
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 (
|
|
12016
|
-
|
|
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
|
-
|
|
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
|