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-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 +4 -0
- 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 +13 -4
- 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/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11990
|
-
|
|
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 (
|
|
12018
|
-
|
|
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
|
-
|
|
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
|