32cd2100d9dd11140705c848850e86c4f79191c4
max
Tue Sep 1 07:02:04 2026 -0700
Reusable "view snapshot" sessions for durable, minimal Share-a-link links
"Share a link" could only share the whole cart: it saved a session holding
every track, setting and position, which bloated the central db and leaked
the sharer's unrelated tracks to whoever opened the link. Anonymous share
sessions (under the reserved user "l") were also never reaped, so they
accumulated forever, and their 8-char names were generated client-side with
no uniqueness check, so two shares could collide and silently overwrite.
Adds a lightweight "snapshot session" facility (lib/snapshotSession.c): a
snapshot stores only the handful of cart variables a feature declares (a
registered snapshotType, e.g. "blat" -> {db, blatLastBigBed}), moves only
those variables' trash files into durable sessionData storage, and is saved
under a "__"-prefixed name. The "__" marks it machine-made: hidden from the
My Sessions list by default and eligible for reaping. For the anonymous "l"
owner the durable files fan out over two extra hash levels so one directory
never fills with millions of entries.
Every anonymous link now shares one server-side name generator
(snapshotNewName): a unique (db-checked), crypto-strong, "__"-prefixed token,
so tokens never collide. The hgSession doSaveSessionJson endpoint gained
hgS_snapshotType (save a minimal snapshot rather than the whole cart) and
hgS_doAnonName (reserve a unique anonymous name without saving, so the
top-right dialog can preview the exact link before it is created).
Wired three callers to the facility:
- hgc htcBlatAlign "Share a link": a minimal "blat" snapshot instead of a
full-cart anonymous session.
- hgBlat results "Share a link": creates a "blat" snapshot on click and
reveals its ?u=&s= reopen link (rebuilt from the durable bigPsl by the
existing doShareReopen), replacing the trash-only reveal.
- top-right "Share a link": anonymous links use the reserved server name;
logged-in named shares are unchanged.
snapshotReaper (hg/utils) garbage-collects abandoned anonymous snapshots: it
deletes user "l" "__" rows whose lastUse is older than the TTL (hg.conf
snapshot.ttlDays, default ~4 years) and removes their durable files. lastUse
is bumped on every open by the existing session load, so a link stays alive
as long as it is used. Meant to run from the trash-cleaner cron.
Also folds in the recent Share-dialog work in these files: the auto share
name is a short "_" prefix instead of "share_", the dialog previews the link
and creates it only when the button is clicked (no orphan session just from
opening the dialog), an optional name field with an overwrite warning, and
the anonymous save path forces the reap-eligible "__" name.
refs #38197
diff --git src/hg/js/hgBlat.js src/hg/js/hgBlat.js
index ff1122b26a3..092cbb5bd71 100644
--- src/hg/js/hgBlat.js
+++ src/hg/js/hgBlat.js
@@ -108,42 +108,43 @@
} else {
stats = stat('Query', htmlEncode(cfg.queryName)) + div +
stat('Length', blatFmt(cfg.querySize) + ' ' + blatUnit()) + div + assembly;
}
var actions = '';
// "View all in browser" is the primary action, so it comes first.
if (cfg.viewAllUrl) {
actions += `<a class="gbPill" title="Open the Genome Browser with all these BLAT hits shown together as one custom track" href="${htmlEncode(cfg.viewAllUrl)}">View all in browser</a>`;
}
// "Show Query Sequence" opens the query FASTA in a panel (with Download / Copy). Only on a fresh
// search, where the uploaded sequence is available (cfg.querySeqs emitted by hgBlat.c).
if (cfg.querySeqs && cfg.querySeqs.length) {
actions += '<button type="button" class="gbPill" id="blatSeqBtn" ' +
'title="Show the sequence you searched with, in FASTA format">Show Query Sequence</button>';
}
- // "Share a link" just reveals the page's stable URL (cfg.shareUrl, a trash-backed reopen link).
- // cfg.canShare covers old session-based links (?u=&s=), where the current URL is already shareable.
- if (cfg.shareUrl || cfg.canShare) {
+ // "Share a link" creates a durable, minimal snapshot session (db + results bigPsl only) and shows
+ // its ?u=&s= reopen link (see blatShareLink). Only offered when a durable bigPsl backs the
+ // results (cfg.canShare = autoBigPsl); without it there is nothing for the shared link to reopen.
+ if (cfg.canShare) {
// A small share-nodes icon precedes the label so users learn to associate it with sharing.
var shareIcon = '<svg class="blatShareIcon" viewBox="0 0 24 24" width="13" height="13" ' +
'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
'stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"></circle>' +
'<circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle>' +
'<line x1="8.6" y1="10.5" x2="15.4" y2="6.5"></line>' +
'<line x1="8.6" y1="13.5" x2="15.4" y2="17.5"></line></svg>';
actions += '<button type="button" class="gbPill" id="blatShareBtn" ' +
- 'title="Show a link that reopens these results (works for a limited time)">' +
+ 'title="Create a durable link that reopens these BLAT results">' +
shareIcon + 'Share a link</button>';
}
// "Rename BLAT Track" opens a modal to rename the results custom track. This is a JS-native
// button (renders immediately with the strip) that replaces the old C-emitted inline form, which
// only appeared after the buildBigPsl AJAX finished and reflowed the page when clicked.
if (cfg.canRename) {
actions += '<button type="button" class="gbPill" id="blatRenameBtn" ' +
'title="Rename this BLAT results custom track and its description">Rename BLAT Track</button>';
}
return `<div class="gbStrip">${stats}<span class="gbStripActions">${actions}</span></div>`;
}
var BLAT_TILE_TIPS = {
'Score': 'BLAT score: matches minus mismatches and gap penalties. Higher is better.',
'Identity': 'Percent identity of the aligned bases.',
@@ -259,58 +260,100 @@
'the alignment crosses introns or deletions.'
};
function blatApplyTooltips() {
$('#blatTable thead th').each(function() {
var tip = BLAT_HEADER_TIPS[$(this).text().trim()];
if (tip) { $(this).attr('title', tip); }
});
if (typeof convertTitleTagsToMouseovers === 'function') {
convertTitleTagsToMouseovers();
}
}
// ---- share a link --------------------------------------------------------
-function blatShareLink() {
- // No session, no AJAX: the results page already has a stable, shareable URL (hgBlat.c emits it as
- // cfg.shareUrl and blatBuild() pins it into the address bar with history.replaceState), so this
- // just shows/copies window.location. The link reopens straight from the trash .pslx/.fa, so it
- // works until those trash files are cleaned - hence the retention note.
- var box = document.getElementById('gbShareBox');
- if (!box) { return; }
- if (box.style.display === 'flex') { box.style.display = 'none'; return; } // toggle off
- var url = window.location.href;
+// The snapshot link we created for this page view, cached so re-opening the box doesn't make another.
+var blatShareCachedUrl = null;
+
+// Render the share box. url set -> show the link + Copy; url null -> "Creating link…"; msg (url null)
+// -> show an error.
+function blatShowShareBox(box, url, msg) {
box.style.display = 'flex';
+ if (msg) {
+ box.innerHTML = '<span class="gbShareMsg gbShareFull" style="color:#a00">' +
+ htmlEncode(msg) + '</span>';
+ return;
+ }
+ if (!url) {
+ box.innerHTML = '<span class="gbShareMsg gbShareFull">Creating link…</span>';
+ return;
+ }
box.innerHTML =
- '<span class="gbShareMsg gbShareFull">Shareable link — anyone with it can reopen ' +
- 'these results. The results are stored temporarily, so the link works for at least 48 hours ' +
- 'after they were last viewed.</span>' +
+ '<span class="gbShareMsg gbShareFull">Shareable link — anyone with it can reopen these ' +
+ 'BLAT results. It stores only the results (not your other tracks or settings) and stays ' +
+ 'active as long as it is used.</span>' +
'<input id="gbShareInput" class="gbShareInput" type="text" readonly>' +
'<button type="button" class="gbPill" id="blatShareCopy" title="Copy the link to the clipboard">Copy</button>';
var inp = document.getElementById('gbShareInput');
inp.value = url;
inp.focus();
inp.select();
$('#blatShareCopy').on('click', function() {
inp.select();
if (navigator.clipboard) { navigator.clipboard.writeText(url); }
else { document.execCommand('copy'); }
this.textContent = 'Copied';
});
}
+function blatShareLink() {
+ // Create (or reveal) a durable share link. It is backed by a lightweight "snapshot" session that
+ // stores only db + the results bigPsl - not the whole cart - under a server-generated unique name
+ // (see lib/snapshotSession.c). hgBlat's ?u=&s= reopen (doShareReopen) rebuilds the results table
+ // from that bigPsl. The token generation, uniqueness and reaping are shared with hgc and the
+ // top-right "Share a link".
+ var box = document.getElementById('gbShareBox');
+ if (!box) { return; }
+ if (box.style.display === 'flex') { box.style.display = 'none'; return; } // toggle off
+
+ // Already viewing a shared session link: the current URL is itself the shareable link.
+ if (/[?&]s=/.test(window.location.search)) { blatShowShareBox(box, window.location.href); return; }
+ // Already created one this page view: reuse it rather than creating another session.
+ if (blatShareCachedUrl) { blatShowShareBox(box, blatShareCachedUrl); return; }
+
+ var cfg = hgBlatData.config;
+ blatShowShareBox(box, null); // "Creating link…"
+ var body = 'hgsid=' + encodeURIComponent(cfg.hgsid || '') +
+ '&hgS_doSaveSessionJson=1&hgS_shareAnon=1&hgS_snapshotType=blat';
+ fetch('../cgi-bin/hgSession', {method: 'POST', credentials: 'same-origin',
+ headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: body})
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (!data || !data.name) {
+ blatShowShareBox(box, null, (data && data.error) || 'Could not create the link.');
+ return;
+ }
+ blatShareCachedUrl = window.location.origin + '/cgi-bin/hgBlat?u=l&s=' +
+ encodeURIComponent(data.name);
+ blatShowShareBox(box, blatShareCachedUrl);
+ })
+ .catch(function() {
+ blatShowShareBox(box, null, 'Could not reach the server. Please try again.');
+ });
+}
+
// ---- Rename BLAT track (modal) -------------------------------------------
// The results custom track is built (and renamed) by hgBlat.c's inline code, which exposes a small
// window.blatRenameCt(name, description) helper (it POSTs to hgc's buildBigPsl and rebuilds the
// track). We reuse that helper (no new endpoint), just swapping its old inline toggle-form UI for a
// proper modal dialog. The current name/description come from cfg (hgBlat.c), not a global, so this
// does not depend on any generic page-global.
function blatRenameModalHtml(cfg) {
// hgSession link is relative (same /cgi-bin/), carrying db + hgsid so the session page opens in
// this assembly and cart.
var sessionUrl = `hgSession?db=${encodeURIComponent(cfg.db)}&hgsid=${encodeURIComponent(cfg.hgsid)}`;
return '<div id="gbModalBg" class="gbModalBg" style="display:none">' +
'<div class="gbModal" role="dialog" aria-modal="true" aria-labelledby="gbModalTitle">' +
'<div class="gbModalTitle" id="gbModalTitle">Rename BLAT Track</div>' +
'<div class="gbModalText">Every BLAT result is stored in its own track in the Genome ' +