@cmdcolin/jbrowse-plugin-hubs 1.0.12 → 1.0.13

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/index.js CHANGED
@@ -23,45 +23,107 @@ function getGenArkConfigUrl(accession) {
23
23
  const [b1, b2, b3] = match;
24
24
  return `https://jbrowse.org/hubs/genark/${base}/${b1}/${b2}/${b3}/${accession}/config.json`;
25
25
  }
26
- function getConfigUrl(assemblyName) {
26
+ // Candidate urls for an assembly name, most preferred first. The probe below
27
+ // takes the first that exists, so a name with no hosted config anywhere is
28
+ // still answered by staying quiet.
29
+ //
30
+ // A UCSC db gets `minimal.json` ahead of `config.json`. Both carry the same
31
+ // `assemblies` block (minimal.json is a filtered copy of config.json, tracks
32
+ // dropped to NCBI RefSeq / GENCODE / RepeatMasker / gaps), and the assembly is
33
+ // the whole reason this connection exists — the tracks are a bonus for the
34
+ // panel it opens. The full config is an expensive way to learn a sequence
35
+ // adapter: hg38 is 2.1MB against minimal's 300KB, hg19 1.35MB against 57KB,
36
+ // hs1 603KB against 12KB, and the parse is followed by MST instantiating every
37
+ // track config in it on the main thread (hg38 595 against 33, hs1 624 against
38
+ // 7). hg38 alone names 239 distinct mate assemblies across its 239 synteny
39
+ // tracks, so a session that opens a few of them pays this several times over.
40
+ //
41
+ // What minimal.json costs is the mate genome's long tail — conservation,
42
+ // expression, the rest of the hub. Those are still one "Add connection" away
43
+ // on the full config url, and nothing about this panel implied they were
44
+ // coming. GenArk hubs have no minimal.json and don't need one; their whole
45
+ // config is ~40KB.
46
+ function getConfigUrls(assemblyName) {
27
47
  if (assemblyName.startsWith('GCA_') || assemblyName.startsWith('GCF_')) {
28
- return getGenArkConfigUrl(assemblyName);
48
+ const url = getGenArkConfigUrl(assemblyName);
49
+ return url ? [url] : [];
29
50
  }
30
- return `https://jbrowse.org/ucsc/${assemblyName}/config.json`;
51
+ const base = `https://jbrowse.org/ucsc/${assemblyName}`;
52
+ return [`${base}/minimal.json`, `${base}/config.json`];
31
53
  }
32
- // Names already being resolved, so the extension point firing repeatedly for
33
- // the same assembly (it fires on every unresolved read) starts one probe, not
34
- // one per read. The connection itself is deduped by connectionId, but that
35
- // check only happens once the probe has come back.
36
- const pending = new Set();
37
- // The url is a guess from the name, and plenty of assemblies aren't at it:
38
- // GenArk strain hubs (mouseStrains and friends) live under
39
- // /hubs/genark/<hub>/<strain>/, and a session can name an assembly this host
40
- // has never heard of. Connecting to a config that 404s puts a red error
41
- // snackbar over a session that is otherwise working, so probe first and stay
42
- // quiet when there's nothing there — some other handler, or the session
43
- // itself, may be supplying the assembly.
44
- async function connectIfConfigExists(session, assemblyName, uri) {
54
+ // Every assembly name this plugin has already taken a run at, whether the probe
55
+ // is still in flight or came back with nothing. The extension point fires on an
56
+ // unresolved read, and `get` is read from render paths, so a name that resolves
57
+ // to no hosted config is asked about again and again — GenArk strain hubs
58
+ // (mouseStrains and friends) live under /hubs/genark/<hub>/<strain>/ and are not
59
+ // at the url guessed from the name, and a session can name an assembly this host
60
+ // has never heard of at all. Clearing this once the probe settled (what an
61
+ // earlier `finally` did) meant the next read re-probed, i.e. one HEAD per render
62
+ // forever for exactly the names nothing can supply.
63
+ //
64
+ // A name that DID resolve is also in here, and is separately deduped by
65
+ // connectionId once the connection exists. Nothing is ever removed within a
66
+ // session: a probe answers a question about a url that does not change within a
67
+ // page load.
68
+ //
69
+ // Keyed by session, because the connection a probe created belongs to the
70
+ // session and goes with it. A flat Set would remember a name across
71
+ // `setSession` and leave the new session unable to resolve a genome the old one
72
+ // had. WeakMap so a replaced session is still collectable.
73
+ //
74
+ // jbrowse-core@main reports each unknown name to the extension point once per
75
+ // session, which makes this redundant there — but this plugin runs against
76
+ // whatever core the host page shipped, down to the v4.0.0 floor, where it is
77
+ // the only thing standing between a mistyped assembly name and an unbounded
78
+ // request stream.
79
+ const attempted = new WeakMap();
80
+ function markAttempted(session, assemblyName) {
81
+ let names = attempted.get(session);
82
+ if (!names) {
83
+ names = new Set();
84
+ attempted.set(session, names);
85
+ }
86
+ if (names.has(assemblyName)) {
87
+ return false;
88
+ }
89
+ names.add(assemblyName);
90
+ return true;
91
+ }
92
+ // Connect to the first of `uris` that exists. Connecting to a config that 404s
93
+ // puts a red error snackbar over a session that is otherwise working, so probe
94
+ // first and stay quiet when there's nothing there — some other handler, or the
95
+ // session itself, may be supplying the assembly.
96
+ async function connectIfConfigExists(session, assemblyName, uris) {
45
97
  const connectionId = `jb2hub-${assemblyName}`;
46
- if (!pending.has(assemblyName) &&
47
- !session.connections.find(f => f.connectionId === connectionId)) {
48
- pending.add(assemblyName);
49
- try {
50
- const response = await fetch(uri, { method: 'HEAD' });
51
- if (response.ok) {
52
- const conf = {
53
- type: 'JB2TrackHubConnection',
54
- uri,
55
- name: `conn_${assemblyName}`,
56
- assemblyNames: [assemblyName],
57
- connectionId,
58
- };
59
- session.addConnectionConf(conf);
60
- session.makeConnection(conf);
61
- }
62
- }
63
- finally {
64
- pending.delete(assemblyName);
98
+ if (session.connections.find(f => f.connectionId === connectionId) ||
99
+ !markAttempted(session, assemblyName)) {
100
+ return;
101
+ }
102
+ for (const uri of uris) {
103
+ const response = await fetch(uri, { method: 'HEAD' });
104
+ if (response.ok) {
105
+ const conf = {
106
+ type: 'JB2TrackHubConnection',
107
+ uri,
108
+ name: `conn_${assemblyName}`,
109
+ assemblyNames: [assemblyName],
110
+ connectionId,
111
+ };
112
+ session.addConnectionConf(conf);
113
+ // `silent`: no "Successfully loaded" snackbar. Nobody asked for this
114
+ // connection — it exists because a track referenced an assembly the
115
+ // session didn't have — so announcing it is noise over whatever the
116
+ // user was doing, and it lands on top of any screenshot of a hub
117
+ // genome. The property is newer than every released core (it landed
118
+ // after v4.3.0) and this plugin runs against whatever core the host
119
+ // page shipped, down to the v4.0.0 floor, but passing it is safe
120
+ // regardless: MST drops an undeclared key off a model snapshot rather
121
+ // than rejecting it (verified against the fork — the instance is
122
+ // created, the key is neither readable on it nor in its snapshot), so
123
+ // an older core makes the same connection it always did and keeps its
124
+ // toast.
125
+ session.makeConnection(conf, { silent: true });
126
+ return;
65
127
  }
66
128
  }
67
129
  }
@@ -72,12 +134,12 @@ export default class HubsViewerPlugin extends Plugin {
72
134
  pluginManager.addToExtensionPoint('Core-handleUnrecognizedAssembly', (defaultResult, args) => {
73
135
  const session = args.session;
74
136
  const assemblyName = args.assemblyName;
75
- const uri = session && assemblyName && getConfigUrl(assemblyName);
76
- if (session && assemblyName && uri) {
137
+ const uris = assemblyName ? getConfigUrls(assemblyName) : [];
138
+ if (session && assemblyName && uris.length > 0) {
77
139
  // the extension point is sync; the probe and the connection it may
78
140
  // create land later, which is fine because assemblyManager re-reads
79
141
  // reactively once the assembly shows up
80
- connectIfConfigExists(session, assemblyName, uri).catch((e) => {
142
+ connectIfConfigExists(session, assemblyName, uris).catch((e) => {
81
143
  console.error(e);
82
144
  });
83
145
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";var JBrowsePluginHubs=(()=>{var v=Object.create;var a=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var T=Object.getPrototypeOf,D=Object.prototype.hasOwnProperty;var u=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),$=(n,e)=>{for(var o in e)a(n,o,{get:e[o],enumerable:!0})},m=(n,e,o,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of I(e))!D.call(n,s)&&s!==o&&a(n,s,{get:()=>e[s],enumerable:!(t=E(e,s))||t.enumerable});return n};var g=(n,e,o)=>(o=n!=null?v(T(n)):{},m(e||!n||!n.__esModule?a(o,"default",{value:n,enumerable:!0}):o,n)),B=n=>m(a({},"__esModule",{value:!0}),n);var w=u((H,b)=>{b.exports=JBrowseExports["@jbrowse/core/Plugin"]});var h=u((W,k)=>{k.exports=JBrowseExports["@jbrowse/core/util"]});var x=u((z,j)=>{j.exports=JBrowseExports["mobx-state-tree"]});var G={};$(G,{default:()=>l});var S=g(w()),p=g(h()),i=g(x());function A(n,e){let o=URL.createObjectURL(n),t=document.createElement("a");t.href=o,t.download=e,t.rel="noopener",t.click(),setTimeout(()=>{URL.revokeObjectURL(o)},4e4)}function d(n){return Array.isArray(n)?n:[]}function y(n,e,o){let t=new Map;for(let s of[...n,...e])t.set(s[o],s);return[...t.values()]}function R(n,e){let{sessionTracks:o,sessionAssemblies:t,...s}=e;return{...n,tracks:y(d(n.tracks),d(o),"trackId"),assemblies:y(d(n.assemblies),d(t),"name"),defaultSession:s}}var C="1.0.12";function U(n){let{jbrowse:e}=(0,i.getRoot)(n),o=R((0,i.getSnapshot)(e),(0,i.getSnapshot)(n));A(new Blob([JSON.stringify(o,null,2)],{type:"application/json"}),"session.jbrowse")}function O(n){let[e,o]=n.split("_");if(!o)return;let t=o.match(/.{1,3}/g);if(!t||t.length<3)return;let[s,r,c]=t;return`https://jbrowse.org/hubs/genark/${e}/${s}/${r}/${c}/${n}/config.json`}function J(n){return n.startsWith("GCA_")||n.startsWith("GCF_")?O(n):`https://jbrowse.org/ucsc/${n}/config.json`}var f=new Set;async function _(n,e,o){let t=`jb2hub-${e}`;if(!f.has(e)&&!n.connections.find(s=>s.connectionId===t)){f.add(e);try{if((await fetch(o,{method:"HEAD"})).ok){let r={type:"JB2TrackHubConnection",uri:o,name:`conn_${e}`,assemblyNames:[e],connectionId:t};n.addConnectionConf(r),n.makeConnection(r)}}finally{f.delete(e)}}}var l=class extends S.default{name="HubsViewerPlugin";version=C;install(e){e.addToExtensionPoint("Core-handleUnrecognizedAssembly",(o,t)=>{let s=t.session,r=t.assemblyName,c=s&&r&&J(r);return s&&r&&c&&_(s,r,c).catch(M=>{console.error(M)}),o})}configure(e){if(!p.isElectron&&(0,p.isAbstractMenuManager)(e.rootModel))try{e.rootModel.appendToMenu("Tools",{label:"Download desktop session (.jbrowse)",onClick:o=>{U(o)}})}catch(o){console.error("could not add the desktop-session download item",o)}}};return B(G);})();
1
+ "use strict";var JBrowsePluginHubs=(()=>{var v=Object.create;var a=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var I=Object.getPrototypeOf,T=Object.prototype.hasOwnProperty;var l=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),D=(n,e)=>{for(var t in e)a(n,t,{get:e[t],enumerable:!0})},m=(n,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of $(e))!T.call(n,s)&&s!==t&&a(n,s,{get:()=>e[s],enumerable:!(o=E(e,s))||o.enumerable});return n};var g=(n,e,t)=>(t=n!=null?v(I(n)):{},m(e||!n||!n.__esModule?a(t,"default",{value:n,enumerable:!0}):t,n)),B=n=>m(a({},"__esModule",{value:!0}),n);var b=l((H,f)=>{f.exports=JBrowseExports["@jbrowse/core/Plugin"]});var k=l((z,w)=>{w.exports=JBrowseExports["@jbrowse/core/util"]});var j=l((F,h)=>{h.exports=JBrowseExports["mobx-state-tree"]});var L={};D(L,{default:()=>u});var C=g(b()),d=g(k()),c=g(j());function y(n,e){let t=URL.createObjectURL(n),o=document.createElement("a");o.href=t,o.download=e,o.rel="noopener",o.click(),setTimeout(()=>{URL.revokeObjectURL(t)},4e4)}function p(n){return Array.isArray(n)?n:[]}function x(n,e,t){let o=new Map;for(let s of[...n,...e])o.set(s[t],s);return[...o.values()]}function S(n,e){let{sessionTracks:t,sessionAssemblies:o,...s}=e;return{...n,tracks:x(p(n.tracks),p(t),"trackId"),assemblies:x(p(n.assemblies),p(o),"name"),defaultSession:s}}var A="1.0.13";function U(n){let{jbrowse:e}=(0,c.getRoot)(n),t=S((0,c.getSnapshot)(e),(0,c.getSnapshot)(n));y(new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),"session.jbrowse")}function O(n){let[e,t]=n.split("_");if(!t)return;let o=t.match(/.{1,3}/g);if(!o||o.length<3)return;let[s,r,i]=o;return`https://jbrowse.org/hubs/genark/${e}/${s}/${r}/${i}/${n}/config.json`}function J(n){if(n.startsWith("GCA_")||n.startsWith("GCF_")){let t=O(n);return t?[t]:[]}let e=`https://jbrowse.org/ucsc/${n}`;return[`${e}/minimal.json`,`${e}/config.json`]}var R=new WeakMap;function _(n,e){let t=R.get(n);return t||(t=new Set,R.set(n,t)),t.has(e)?!1:(t.add(e),!0)}async function G(n,e,t){let o=`jb2hub-${e}`;if(!(n.connections.find(s=>s.connectionId===o)||!_(n,e))){for(let s of t)if((await fetch(s,{method:"HEAD"})).ok){let i={type:"JB2TrackHubConnection",uri:s,name:`conn_${e}`,assemblyNames:[e],connectionId:o};n.addConnectionConf(i),n.makeConnection(i,{silent:!0});return}}}var u=class extends C.default{name="HubsViewerPlugin";version=A;install(e){e.addToExtensionPoint("Core-handleUnrecognizedAssembly",(t,o)=>{let s=o.session,r=o.assemblyName,i=r?J(r):[];return s&&r&&i.length>0&&G(s,r,i).catch(M=>{console.error(M)}),t})}configure(e){if(!d.isElectron&&(0,d.isAbstractMenuManager)(e.rootModel))try{e.rootModel.appendToMenu("Tools",{label:"Download desktop session (.jbrowse)",onClick:t=>{U(t)}})}catch(t){console.error("could not add the desktop-session download item",t)}}};return B(L);})();
2
2
  //# sourceMappingURL=jbrowse-plugin-hubs.umd.production.min.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["global-externals:@jbrowse/core/Plugin", "global-externals:@jbrowse/core/util", "global-externals:@jbrowse/mobx-state-tree", "../src/index.ts", "../src/util.ts", "../package.json"],
4
- "sourcesContent": ["module.exports = JBrowseExports[\"@jbrowse/core/Plugin\"];", "module.exports = JBrowseExports[\"@jbrowse/core/util\"];", "module.exports = JBrowseExports[\"mobx-state-tree\"];", "import Plugin from '@jbrowse/core/Plugin'\nimport { isAbstractMenuManager, isElectron } from '@jbrowse/core/util'\nimport { getRoot, getSnapshot } from '@jbrowse/mobx-state-tree'\n\nimport { saveAs, toDesktopSnapshot } from './util'\nimport { version } from '../package.json'\n\nimport type PluginManager from '@jbrowse/core/PluginManager'\nimport type { AbstractSessionModel } from '@jbrowse/core/util'\nimport type { IAnyStateTreeNode } from '@jbrowse/mobx-state-tree'\n\ninterface Session {\n makeConnection: (conf: unknown) => void\n addConnectionConf: (conf: unknown) => void\n connections: { connectionId: string }[]\n}\n\n// Serializes the current web session as a desktop `.jbrowse` file. genomes\n// sessions are config-shaped already, so this just folds the live session into\n// a defaultSession and downloads it; desktop opens it via File > Open session.\nfunction downloadDesktopSession(session: AbstractSessionModel) {\n const { jbrowse } = getRoot<{ jbrowse: IAnyStateTreeNode }>(session)\n const snap = toDesktopSnapshot(\n getSnapshot<Record<string, unknown>>(jbrowse),\n getSnapshot<Record<string, unknown>>(session),\n )\n saveAs(\n new Blob([JSON.stringify(snap, null, 2)], { type: 'application/json' }),\n 'session.jbrowse',\n )\n}\n\nfunction getGenArkConfigUrl(accession: string) {\n const [base, rest] = accession.split('_')\n if (!rest) {\n return undefined\n }\n const match = rest.match(/.{1,3}/g)\n if (!match || match.length < 3) {\n return undefined\n }\n const [b1, b2, b3] = match\n return `https://jbrowse.org/hubs/genark/${base}/${b1}/${b2}/${b3}/${accession}/config.json`\n}\n\nfunction getConfigUrl(assemblyName: string) {\n if (assemblyName.startsWith('GCA_') || assemblyName.startsWith('GCF_')) {\n return getGenArkConfigUrl(assemblyName)\n }\n return `https://jbrowse.org/ucsc/${assemblyName}/config.json`\n}\n\n// Names already being resolved, so the extension point firing repeatedly for\n// the same assembly (it fires on every unresolved read) starts one probe, not\n// one per read. The connection itself is deduped by connectionId, but that\n// check only happens once the probe has come back.\nconst pending = new Set<string>()\n\n// The url is a guess from the name, and plenty of assemblies aren't at it:\n// GenArk strain hubs (mouseStrains and friends) live under\n// /hubs/genark/<hub>/<strain>/, and a session can name an assembly this host\n// has never heard of. Connecting to a config that 404s puts a red error\n// snackbar over a session that is otherwise working, so probe first and stay\n// quiet when there's nothing there \u2014 some other handler, or the session\n// itself, may be supplying the assembly.\nasync function connectIfConfigExists(\n session: Session,\n assemblyName: string,\n uri: string,\n) {\n const connectionId = `jb2hub-${assemblyName}`\n if (\n !pending.has(assemblyName) &&\n !session.connections.find(f => f.connectionId === connectionId)\n ) {\n pending.add(assemblyName)\n try {\n const response = await fetch(uri, { method: 'HEAD' })\n if (response.ok) {\n const conf = {\n type: 'JB2TrackHubConnection',\n uri,\n name: `conn_${assemblyName}`,\n assemblyNames: [assemblyName],\n connectionId,\n }\n session.addConnectionConf(conf)\n session.makeConnection(conf)\n }\n } finally {\n pending.delete(assemblyName)\n }\n }\n}\n\nexport default class HubsViewerPlugin extends Plugin {\n name = 'HubsViewerPlugin'\n version = version\n\n install(pluginManager: PluginManager) {\n pluginManager.addToExtensionPoint(\n 'Core-handleUnrecognizedAssembly',\n (defaultResult, args) => {\n const session = args.session as Session | undefined\n const assemblyName = args.assemblyName as string | undefined\n const uri = session && assemblyName && getConfigUrl(assemblyName)\n if (session && assemblyName && uri) {\n // the extension point is sync; the probe and the connection it may\n // create land later, which is fine because assemblyManager re-reads\n // reactively once the assembly shows up\n connectIfConfigExists(session, assemblyName, uri).catch(\n (e: unknown) => {\n console.error(e)\n },\n )\n }\n // Pass the accumulator through: this extension point chains handlers, so\n // returning undefined would clobber another plugin's result.\n return defaultResult\n },\n )\n }\n\n configure(pluginManager: PluginManager) {\n // jbrowse-web only. In desktop you'd just save the session, and an exported\n // .jbrowse file carries this plugin in its plugins list, so an older desktop\n // would otherwise load and run this menu code when opening that file.\n if (!isElectron && isAbstractMenuManager(pluginManager.rootModel)) {\n // 'Tools', not 'File'. Every released core from v4.0.0 to latest defines\n // the File menu as a thunk and appends with `menu.menuItems.push()`, which\n // throws on a function; the throw escapes configure() and renders the app\n // as an error page rather than costing just this menu item. Because the\n // plugin store serves `latest/` no-cache, that took out every\n // jbrowse.org/ucsc launch on hg38/hg19/mm39/hs1 the moment 1.0.9 shipped.\n // core@main resolves item contributions lazily and accepts either form,\n // but this plugin has to keep working on the releases already in the wild.\n //\n // Belt and braces on the same reasoning: a cosmetic menu item should never\n // be able to cost a user their whole session.\n try {\n pluginManager.rootModel.appendToMenu('Tools', {\n label: 'Download desktop session (.jbrowse)',\n onClick: (session: AbstractSessionModel) => {\n downloadDesktopSession(session)\n },\n })\n } catch (e) {\n console.error('could not add the desktop-session download item', e)\n }\n }\n }\n}\n", "// Trigger a browser download of `blob` as `filename`. Mirrors the anchor-download\n// path of @jbrowse/core's saveAs: the deferred revoke avoids canceling the\n// download before the browser has read the blob (the read is async in some\n// browsers). Inlined until a published core release exports saveAs from its util.\nexport function saveAs(blob: Blob, filename: string) {\n const url = URL.createObjectURL(blob)\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename\n anchor.rel = 'noopener'\n anchor.click()\n setTimeout(() => {\n URL.revokeObjectURL(url)\n }, 40000)\n}\n\nfunction asObjArray(value: unknown): Record<string, unknown>[] {\n return Array.isArray(value) ? (value as Record<string, unknown>[]) : []\n}\n\n// Merges two lists of config objects by an id key, with `overrides` winning so\n// a session-scoped track/assembly that shadows a config one (same id) replaces\n// it rather than duplicating.\nfunction mergeById(\n base: Record<string, unknown>[],\n overrides: Record<string, unknown>[],\n key: string,\n): Record<string, unknown>[] {\n const merged = new Map<unknown, Record<string, unknown>>()\n for (const item of [...base, ...overrides]) {\n merged.set(item[key], item)\n }\n return [...merged.values()]\n}\n\n// Builds a desktop `.jbrowse` snapshot (a config plus a defaultSession) from a\n// web root config and its live session. Session-scoped tracks/assemblies are\n// hoisted into the config arrays so they open as first-class config entries in\n// desktop; the rest of the session becomes the defaultSession. Web sessions are\n// always UriLocation-based, so the result is fully portable to desktop.\nexport function toDesktopSnapshot(\n jbrowse: Record<string, unknown>,\n session: Record<string, unknown>,\n): Record<string, unknown> {\n const { sessionTracks, sessionAssemblies, ...defaultSession } = session\n return {\n ...jbrowse,\n tracks: mergeById(\n asObjArray(jbrowse.tracks),\n asObjArray(sessionTracks),\n 'trackId',\n ),\n assemblies: mergeById(\n asObjArray(jbrowse.assemblies),\n asObjArray(sessionAssemblies),\n 'name',\n ),\n defaultSession,\n }\n}\n", "{\n \"version\": \"1.0.12\",\n \"license\": \"MIT\",\n \"name\": \"@cmdcolin/jbrowse-plugin-hubs\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/GMOD/jbrowse-plugin-hubs.git\"\n },\n \"keywords\": [\n \"jbrowse\",\n \"jbrowse2\"\n ],\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.esm.js\",\n \"typings\": \"dist/index.d.ts\",\n \"files\": [\n \"dist\",\n \"src\"\n ],\n \"scripts\": {\n \"clean\": \"rimraf dist\",\n \"start\": \"node esbuild.mjs --watch\",\n \"test\": \"echo 'no tests'\",\n \"format\": \"pnpm prettier --write .\",\n \"prebuild\": \"pnpm clean\",\n \"build\": \"tsc && NODE_ENV=production node esbuild.mjs\",\n \"lint\": \"eslint src --report-unused-disable-directives --max-warnings 0\",\n \"prepack\": \"pnpm build\",\n \"postversion\": \"git push --follow-tags\",\n \"preversion\": \"pnpm lint\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^10.0.1\",\n \"@fal-works/esbuild-plugin-global-externals\": \"^2.1.2\",\n \"@jbrowse/core\": \"^4.3.0\",\n \"@jbrowse/mobx-state-tree\": \"^5.11.1\",\n \"@mui/material\": \"^7.3.11\",\n \"@mui/system\": \"^7.3.11\",\n \"@mui/x-data-grid\": \"^8.29.1\",\n \"@types/node\": \"^25.9.3\",\n \"@types/react\": \"^19.2.17\",\n \"@typescript-eslint/eslint-plugin\": \"^8.61.1\",\n \"@typescript-eslint/parser\": \"^8.61.1\",\n \"esbuild\": \"^0.27.7\",\n \"eslint\": \"^10.5.0\",\n \"eslint-plugin-import-x\": \"^4.16.2\",\n \"eslint-plugin-react\": \"^7.37.5\",\n \"eslint-plugin-react-hooks\": \"^7.1.1\",\n \"eslint-plugin-unicorn\": \"^64.0.0\",\n \"mobx\": \"^6.16.1\",\n \"mobx-react\": \"^9.2.2\",\n \"prettier\": \"^3.8.4\",\n \"pretty-bytes\": \"^7.1.0\",\n \"react\": \"^19.2.7\",\n \"react-dom\": \"^19.2.7\",\n \"rimraf\": \"^6.1.3\",\n \"rxjs\": \"^7.8.2\",\n \"serve\": \"^14.2.6\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.61.1\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["require_Plugin", "__commonJSMin", "exports", "module", "require_util", "__commonJSMin", "exports", "module", "require_mobx_state_tree", "__commonJSMin", "exports", "module", "index_exports", "__export", "HubsViewerPlugin", "import_Plugin", "import_util", "import_mobx_state_tree", "saveAs", "blob", "filename", "url", "anchor", "asObjArray", "value", "mergeById", "base", "overrides", "key", "merged", "item", "toDesktopSnapshot", "jbrowse", "session", "sessionTracks", "sessionAssemblies", "defaultSession", "version", "downloadDesktopSession", "session", "jbrowse", "snap", "toDesktopSnapshot", "saveAs", "getGenArkConfigUrl", "accession", "base", "rest", "match", "b1", "b2", "b3", "getConfigUrl", "assemblyName", "pending", "connectIfConfigExists", "uri", "connectionId", "f", "conf", "HubsViewerPlugin", "Plugin", "version", "pluginManager", "defaultResult", "args", "e"]
4
+ "sourcesContent": ["module.exports = JBrowseExports[\"@jbrowse/core/Plugin\"];", "module.exports = JBrowseExports[\"@jbrowse/core/util\"];", "module.exports = JBrowseExports[\"mobx-state-tree\"];", "import Plugin from '@jbrowse/core/Plugin'\nimport { isAbstractMenuManager, isElectron } from '@jbrowse/core/util'\nimport { getRoot, getSnapshot } from '@jbrowse/mobx-state-tree'\n\nimport { saveAs, toDesktopSnapshot } from './util'\nimport { version } from '../package.json'\n\nimport type PluginManager from '@jbrowse/core/PluginManager'\nimport type { AbstractSessionModel } from '@jbrowse/core/util'\nimport type { IAnyStateTreeNode } from '@jbrowse/mobx-state-tree'\n\ninterface Session {\n makeConnection: (conf: unknown, initialSnapshot?: unknown) => void\n addConnectionConf: (conf: unknown) => void\n connections: { connectionId: string }[]\n}\n\n// Serializes the current web session as a desktop `.jbrowse` file. genomes\n// sessions are config-shaped already, so this just folds the live session into\n// a defaultSession and downloads it; desktop opens it via File > Open session.\nfunction downloadDesktopSession(session: AbstractSessionModel) {\n const { jbrowse } = getRoot<{ jbrowse: IAnyStateTreeNode }>(session)\n const snap = toDesktopSnapshot(\n getSnapshot<Record<string, unknown>>(jbrowse),\n getSnapshot<Record<string, unknown>>(session),\n )\n saveAs(\n new Blob([JSON.stringify(snap, null, 2)], { type: 'application/json' }),\n 'session.jbrowse',\n )\n}\n\nfunction getGenArkConfigUrl(accession: string) {\n const [base, rest] = accession.split('_')\n if (!rest) {\n return undefined\n }\n const match = rest.match(/.{1,3}/g)\n if (!match || match.length < 3) {\n return undefined\n }\n const [b1, b2, b3] = match\n return `https://jbrowse.org/hubs/genark/${base}/${b1}/${b2}/${b3}/${accession}/config.json`\n}\n\n// Candidate urls for an assembly name, most preferred first. The probe below\n// takes the first that exists, so a name with no hosted config anywhere is\n// still answered by staying quiet.\n//\n// A UCSC db gets `minimal.json` ahead of `config.json`. Both carry the same\n// `assemblies` block (minimal.json is a filtered copy of config.json, tracks\n// dropped to NCBI RefSeq / GENCODE / RepeatMasker / gaps), and the assembly is\n// the whole reason this connection exists \u2014 the tracks are a bonus for the\n// panel it opens. The full config is an expensive way to learn a sequence\n// adapter: hg38 is 2.1MB against minimal's 300KB, hg19 1.35MB against 57KB,\n// hs1 603KB against 12KB, and the parse is followed by MST instantiating every\n// track config in it on the main thread (hg38 595 against 33, hs1 624 against\n// 7). hg38 alone names 239 distinct mate assemblies across its 239 synteny\n// tracks, so a session that opens a few of them pays this several times over.\n//\n// What minimal.json costs is the mate genome's long tail \u2014 conservation,\n// expression, the rest of the hub. Those are still one \"Add connection\" away\n// on the full config url, and nothing about this panel implied they were\n// coming. GenArk hubs have no minimal.json and don't need one; their whole\n// config is ~40KB.\nfunction getConfigUrls(assemblyName: string) {\n if (assemblyName.startsWith('GCA_') || assemblyName.startsWith('GCF_')) {\n const url = getGenArkConfigUrl(assemblyName)\n return url ? [url] : []\n }\n const base = `https://jbrowse.org/ucsc/${assemblyName}`\n return [`${base}/minimal.json`, `${base}/config.json`]\n}\n\n// Every assembly name this plugin has already taken a run at, whether the probe\n// is still in flight or came back with nothing. The extension point fires on an\n// unresolved read, and `get` is read from render paths, so a name that resolves\n// to no hosted config is asked about again and again \u2014 GenArk strain hubs\n// (mouseStrains and friends) live under /hubs/genark/<hub>/<strain>/ and are not\n// at the url guessed from the name, and a session can name an assembly this host\n// has never heard of at all. Clearing this once the probe settled (what an\n// earlier `finally` did) meant the next read re-probed, i.e. one HEAD per render\n// forever for exactly the names nothing can supply.\n//\n// A name that DID resolve is also in here, and is separately deduped by\n// connectionId once the connection exists. Nothing is ever removed within a\n// session: a probe answers a question about a url that does not change within a\n// page load.\n//\n// Keyed by session, because the connection a probe created belongs to the\n// session and goes with it. A flat Set would remember a name across\n// `setSession` and leave the new session unable to resolve a genome the old one\n// had. WeakMap so a replaced session is still collectable.\n//\n// jbrowse-core@main reports each unknown name to the extension point once per\n// session, which makes this redundant there \u2014 but this plugin runs against\n// whatever core the host page shipped, down to the v4.0.0 floor, where it is\n// the only thing standing between a mistyped assembly name and an unbounded\n// request stream.\nconst attempted = new WeakMap<Session, Set<string>>()\n\nfunction markAttempted(session: Session, assemblyName: string) {\n let names = attempted.get(session)\n if (!names) {\n names = new Set()\n attempted.set(session, names)\n }\n if (names.has(assemblyName)) {\n return false\n }\n names.add(assemblyName)\n return true\n}\n\n// Connect to the first of `uris` that exists. Connecting to a config that 404s\n// puts a red error snackbar over a session that is otherwise working, so probe\n// first and stay quiet when there's nothing there \u2014 some other handler, or the\n// session itself, may be supplying the assembly.\nasync function connectIfConfigExists(\n session: Session,\n assemblyName: string,\n uris: string[],\n) {\n const connectionId = `jb2hub-${assemblyName}`\n if (\n session.connections.find(f => f.connectionId === connectionId) ||\n !markAttempted(session, assemblyName)\n ) {\n return\n }\n for (const uri of uris) {\n const response = await fetch(uri, { method: 'HEAD' })\n if (response.ok) {\n const conf = {\n type: 'JB2TrackHubConnection',\n uri,\n name: `conn_${assemblyName}`,\n assemblyNames: [assemblyName],\n connectionId,\n }\n session.addConnectionConf(conf)\n // `silent`: no \"Successfully loaded\" snackbar. Nobody asked for this\n // connection \u2014 it exists because a track referenced an assembly the\n // session didn't have \u2014 so announcing it is noise over whatever the\n // user was doing, and it lands on top of any screenshot of a hub\n // genome. The property is newer than every released core (it landed\n // after v4.3.0) and this plugin runs against whatever core the host\n // page shipped, down to the v4.0.0 floor, but passing it is safe\n // regardless: MST drops an undeclared key off a model snapshot rather\n // than rejecting it (verified against the fork \u2014 the instance is\n // created, the key is neither readable on it nor in its snapshot), so\n // an older core makes the same connection it always did and keeps its\n // toast.\n session.makeConnection(conf, { silent: true })\n return\n }\n }\n}\n\nexport default class HubsViewerPlugin extends Plugin {\n name = 'HubsViewerPlugin'\n version = version\n\n install(pluginManager: PluginManager) {\n pluginManager.addToExtensionPoint(\n 'Core-handleUnrecognizedAssembly',\n (defaultResult, args) => {\n const session = args.session as Session | undefined\n const assemblyName = args.assemblyName as string | undefined\n const uris = assemblyName ? getConfigUrls(assemblyName) : []\n if (session && assemblyName && uris.length > 0) {\n // the extension point is sync; the probe and the connection it may\n // create land later, which is fine because assemblyManager re-reads\n // reactively once the assembly shows up\n connectIfConfigExists(session, assemblyName, uris).catch(\n (e: unknown) => {\n console.error(e)\n },\n )\n }\n // Pass the accumulator through: this extension point chains handlers, so\n // returning undefined would clobber another plugin's result.\n return defaultResult\n },\n )\n }\n\n configure(pluginManager: PluginManager) {\n // jbrowse-web only. In desktop you'd just save the session, and an exported\n // .jbrowse file carries this plugin in its plugins list, so an older desktop\n // would otherwise load and run this menu code when opening that file.\n if (!isElectron && isAbstractMenuManager(pluginManager.rootModel)) {\n // 'Tools', not 'File'. Every released core from v4.0.0 to latest defines\n // the File menu as a thunk and appends with `menu.menuItems.push()`, which\n // throws on a function; the throw escapes configure() and renders the app\n // as an error page rather than costing just this menu item. Because the\n // plugin store serves `latest/` no-cache, that took out every\n // jbrowse.org/ucsc launch on hg38/hg19/mm39/hs1 the moment 1.0.9 shipped.\n // core@main resolves item contributions lazily and accepts either form,\n // but this plugin has to keep working on the releases already in the wild.\n //\n // Belt and braces on the same reasoning: a cosmetic menu item should never\n // be able to cost a user their whole session.\n try {\n pluginManager.rootModel.appendToMenu('Tools', {\n label: 'Download desktop session (.jbrowse)',\n onClick: (session: AbstractSessionModel) => {\n downloadDesktopSession(session)\n },\n })\n } catch (e) {\n console.error('could not add the desktop-session download item', e)\n }\n }\n }\n}\n", "// Trigger a browser download of `blob` as `filename`. Mirrors the anchor-download\n// path of @jbrowse/core's saveAs: the deferred revoke avoids canceling the\n// download before the browser has read the blob (the read is async in some\n// browsers). Inlined until a published core release exports saveAs from its util.\nexport function saveAs(blob: Blob, filename: string) {\n const url = URL.createObjectURL(blob)\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename\n anchor.rel = 'noopener'\n anchor.click()\n setTimeout(() => {\n URL.revokeObjectURL(url)\n }, 40000)\n}\n\nfunction asObjArray(value: unknown): Record<string, unknown>[] {\n return Array.isArray(value) ? (value as Record<string, unknown>[]) : []\n}\n\n// Merges two lists of config objects by an id key, with `overrides` winning so\n// a session-scoped track/assembly that shadows a config one (same id) replaces\n// it rather than duplicating.\nfunction mergeById(\n base: Record<string, unknown>[],\n overrides: Record<string, unknown>[],\n key: string,\n): Record<string, unknown>[] {\n const merged = new Map<unknown, Record<string, unknown>>()\n for (const item of [...base, ...overrides]) {\n merged.set(item[key], item)\n }\n return [...merged.values()]\n}\n\n// Builds a desktop `.jbrowse` snapshot (a config plus a defaultSession) from a\n// web root config and its live session. Session-scoped tracks/assemblies are\n// hoisted into the config arrays so they open as first-class config entries in\n// desktop; the rest of the session becomes the defaultSession. Web sessions are\n// always UriLocation-based, so the result is fully portable to desktop.\nexport function toDesktopSnapshot(\n jbrowse: Record<string, unknown>,\n session: Record<string, unknown>,\n): Record<string, unknown> {\n const { sessionTracks, sessionAssemblies, ...defaultSession } = session\n return {\n ...jbrowse,\n tracks: mergeById(\n asObjArray(jbrowse.tracks),\n asObjArray(sessionTracks),\n 'trackId',\n ),\n assemblies: mergeById(\n asObjArray(jbrowse.assemblies),\n asObjArray(sessionAssemblies),\n 'name',\n ),\n defaultSession,\n }\n}\n", "{\n \"version\": \"1.0.13\",\n \"license\": \"MIT\",\n \"name\": \"@cmdcolin/jbrowse-plugin-hubs\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/GMOD/jbrowse-plugin-hubs.git\"\n },\n \"keywords\": [\n \"jbrowse\",\n \"jbrowse2\"\n ],\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.esm.js\",\n \"typings\": \"dist/index.d.ts\",\n \"files\": [\n \"dist\",\n \"src\"\n ],\n \"scripts\": {\n \"clean\": \"rimraf dist\",\n \"start\": \"node esbuild.mjs --watch\",\n \"test\": \"echo 'no tests'\",\n \"format\": \"pnpm prettier --write .\",\n \"prebuild\": \"pnpm clean\",\n \"build\": \"tsc && NODE_ENV=production node esbuild.mjs\",\n \"lint\": \"eslint src --report-unused-disable-directives --max-warnings 0\",\n \"prepack\": \"pnpm build\",\n \"postversion\": \"git push --follow-tags\",\n \"preversion\": \"pnpm lint && pnpm build && pnpm host-compat\",\n \"host-compat\": \"node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-hubs.umd.production.min.js\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^10.0.1\",\n \"@fal-works/esbuild-plugin-global-externals\": \"^2.1.2\",\n \"@jbrowse/core\": \"^4.3.0\",\n \"@jbrowse/mobx-state-tree\": \"^5.11.1\",\n \"@mui/material\": \"^7.3.11\",\n \"@mui/system\": \"^7.3.11\",\n \"@mui/x-data-grid\": \"^8.29.1\",\n \"@types/node\": \"^25.9.3\",\n \"@types/react\": \"^19.2.17\",\n \"@typescript-eslint/eslint-plugin\": \"^8.61.1\",\n \"@typescript-eslint/parser\": \"^8.61.1\",\n \"esbuild\": \"^0.27.7\",\n \"eslint\": \"^10.5.0\",\n \"eslint-plugin-import-x\": \"^4.16.2\",\n \"eslint-plugin-react\": \"^7.37.5\",\n \"eslint-plugin-react-hooks\": \"^7.1.1\",\n \"eslint-plugin-unicorn\": \"^64.0.0\",\n \"mobx\": \"^6.16.1\",\n \"mobx-react\": \"^9.2.2\",\n \"prettier\": \"^3.8.4\",\n \"pretty-bytes\": \"^7.1.0\",\n \"puppeteer-core\": \"^25.4.0\",\n \"react\": \"^19.2.7\",\n \"react-dom\": \"^19.2.7\",\n \"rimraf\": \"^6.1.3\",\n \"rxjs\": \"^7.8.2\",\n \"serve\": \"^14.2.6\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.61.1\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["require_Plugin", "__commonJSMin", "exports", "module", "require_util", "__commonJSMin", "exports", "module", "require_mobx_state_tree", "__commonJSMin", "exports", "module", "index_exports", "__export", "HubsViewerPlugin", "import_Plugin", "import_util", "import_mobx_state_tree", "saveAs", "blob", "filename", "url", "anchor", "asObjArray", "value", "mergeById", "base", "overrides", "key", "merged", "item", "toDesktopSnapshot", "jbrowse", "session", "sessionTracks", "sessionAssemblies", "defaultSession", "version", "downloadDesktopSession", "session", "jbrowse", "snap", "toDesktopSnapshot", "saveAs", "getGenArkConfigUrl", "accession", "base", "rest", "match", "b1", "b2", "b3", "getConfigUrls", "assemblyName", "url", "attempted", "markAttempted", "names", "connectIfConfigExists", "uris", "connectionId", "f", "uri", "conf", "HubsViewerPlugin", "Plugin", "version", "pluginManager", "defaultResult", "args", "e"]
7
7
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.12",
2
+ "version": "1.0.13",
3
3
  "license": "MIT",
4
4
  "name": "@cmdcolin/jbrowse-plugin-hubs",
5
5
  "repository": {
@@ -27,7 +27,8 @@
27
27
  "lint": "eslint src --report-unused-disable-directives --max-warnings 0",
28
28
  "prepack": "pnpm build",
29
29
  "postversion": "git push --follow-tags",
30
- "preversion": "pnpm lint"
30
+ "preversion": "pnpm lint && pnpm build && pnpm host-compat",
31
+ "host-compat": "node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-hubs.umd.production.min.js"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@eslint/js": "^10.0.1",
@@ -51,6 +52,7 @@
51
52
  "mobx-react": "^9.2.2",
52
53
  "prettier": "^3.8.4",
53
54
  "pretty-bytes": "^7.1.0",
55
+ "puppeteer-core": "^25.4.0",
54
56
  "react": "^19.2.7",
55
57
  "react-dom": "^19.2.7",
56
58
  "rimraf": "^6.1.3",
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ import type { AbstractSessionModel } from '@jbrowse/core/util'
10
10
  import type { IAnyStateTreeNode } from '@jbrowse/mobx-state-tree'
11
11
 
12
12
  interface Session {
13
- makeConnection: (conf: unknown) => void
13
+ makeConnection: (conf: unknown, initialSnapshot?: unknown) => void
14
14
  addConnectionConf: (conf: unknown) => void
15
15
  connections: { connectionId: string }[]
16
16
  }
@@ -43,52 +43,116 @@ function getGenArkConfigUrl(accession: string) {
43
43
  return `https://jbrowse.org/hubs/genark/${base}/${b1}/${b2}/${b3}/${accession}/config.json`
44
44
  }
45
45
 
46
- function getConfigUrl(assemblyName: string) {
46
+ // Candidate urls for an assembly name, most preferred first. The probe below
47
+ // takes the first that exists, so a name with no hosted config anywhere is
48
+ // still answered by staying quiet.
49
+ //
50
+ // A UCSC db gets `minimal.json` ahead of `config.json`. Both carry the same
51
+ // `assemblies` block (minimal.json is a filtered copy of config.json, tracks
52
+ // dropped to NCBI RefSeq / GENCODE / RepeatMasker / gaps), and the assembly is
53
+ // the whole reason this connection exists — the tracks are a bonus for the
54
+ // panel it opens. The full config is an expensive way to learn a sequence
55
+ // adapter: hg38 is 2.1MB against minimal's 300KB, hg19 1.35MB against 57KB,
56
+ // hs1 603KB against 12KB, and the parse is followed by MST instantiating every
57
+ // track config in it on the main thread (hg38 595 against 33, hs1 624 against
58
+ // 7). hg38 alone names 239 distinct mate assemblies across its 239 synteny
59
+ // tracks, so a session that opens a few of them pays this several times over.
60
+ //
61
+ // What minimal.json costs is the mate genome's long tail — conservation,
62
+ // expression, the rest of the hub. Those are still one "Add connection" away
63
+ // on the full config url, and nothing about this panel implied they were
64
+ // coming. GenArk hubs have no minimal.json and don't need one; their whole
65
+ // config is ~40KB.
66
+ function getConfigUrls(assemblyName: string) {
47
67
  if (assemblyName.startsWith('GCA_') || assemblyName.startsWith('GCF_')) {
48
- return getGenArkConfigUrl(assemblyName)
68
+ const url = getGenArkConfigUrl(assemblyName)
69
+ return url ? [url] : []
49
70
  }
50
- return `https://jbrowse.org/ucsc/${assemblyName}/config.json`
71
+ const base = `https://jbrowse.org/ucsc/${assemblyName}`
72
+ return [`${base}/minimal.json`, `${base}/config.json`]
51
73
  }
52
74
 
53
- // Names already being resolved, so the extension point firing repeatedly for
54
- // the same assembly (it fires on every unresolved read) starts one probe, not
55
- // one per read. The connection itself is deduped by connectionId, but that
56
- // check only happens once the probe has come back.
57
- const pending = new Set<string>()
75
+ // Every assembly name this plugin has already taken a run at, whether the probe
76
+ // is still in flight or came back with nothing. The extension point fires on an
77
+ // unresolved read, and `get` is read from render paths, so a name that resolves
78
+ // to no hosted config is asked about again and again — GenArk strain hubs
79
+ // (mouseStrains and friends) live under /hubs/genark/<hub>/<strain>/ and are not
80
+ // at the url guessed from the name, and a session can name an assembly this host
81
+ // has never heard of at all. Clearing this once the probe settled (what an
82
+ // earlier `finally` did) meant the next read re-probed, i.e. one HEAD per render
83
+ // forever for exactly the names nothing can supply.
84
+ //
85
+ // A name that DID resolve is also in here, and is separately deduped by
86
+ // connectionId once the connection exists. Nothing is ever removed within a
87
+ // session: a probe answers a question about a url that does not change within a
88
+ // page load.
89
+ //
90
+ // Keyed by session, because the connection a probe created belongs to the
91
+ // session and goes with it. A flat Set would remember a name across
92
+ // `setSession` and leave the new session unable to resolve a genome the old one
93
+ // had. WeakMap so a replaced session is still collectable.
94
+ //
95
+ // jbrowse-core@main reports each unknown name to the extension point once per
96
+ // session, which makes this redundant there — but this plugin runs against
97
+ // whatever core the host page shipped, down to the v4.0.0 floor, where it is
98
+ // the only thing standing between a mistyped assembly name and an unbounded
99
+ // request stream.
100
+ const attempted = new WeakMap<Session, Set<string>>()
58
101
 
59
- // The url is a guess from the name, and plenty of assemblies aren't at it:
60
- // GenArk strain hubs (mouseStrains and friends) live under
61
- // /hubs/genark/<hub>/<strain>/, and a session can name an assembly this host
62
- // has never heard of. Connecting to a config that 404s puts a red error
63
- // snackbar over a session that is otherwise working, so probe first and stay
64
- // quiet when there's nothing there — some other handler, or the session
65
- // itself, may be supplying the assembly.
102
+ function markAttempted(session: Session, assemblyName: string) {
103
+ let names = attempted.get(session)
104
+ if (!names) {
105
+ names = new Set()
106
+ attempted.set(session, names)
107
+ }
108
+ if (names.has(assemblyName)) {
109
+ return false
110
+ }
111
+ names.add(assemblyName)
112
+ return true
113
+ }
114
+
115
+ // Connect to the first of `uris` that exists. Connecting to a config that 404s
116
+ // puts a red error snackbar over a session that is otherwise working, so probe
117
+ // first and stay quiet when there's nothing there — some other handler, or the
118
+ // session itself, may be supplying the assembly.
66
119
  async function connectIfConfigExists(
67
120
  session: Session,
68
121
  assemblyName: string,
69
- uri: string,
122
+ uris: string[],
70
123
  ) {
71
124
  const connectionId = `jb2hub-${assemblyName}`
72
125
  if (
73
- !pending.has(assemblyName) &&
74
- !session.connections.find(f => f.connectionId === connectionId)
126
+ session.connections.find(f => f.connectionId === connectionId) ||
127
+ !markAttempted(session, assemblyName)
75
128
  ) {
76
- pending.add(assemblyName)
77
- try {
78
- const response = await fetch(uri, { method: 'HEAD' })
79
- if (response.ok) {
80
- const conf = {
81
- type: 'JB2TrackHubConnection',
82
- uri,
83
- name: `conn_${assemblyName}`,
84
- assemblyNames: [assemblyName],
85
- connectionId,
86
- }
87
- session.addConnectionConf(conf)
88
- session.makeConnection(conf)
129
+ return
130
+ }
131
+ for (const uri of uris) {
132
+ const response = await fetch(uri, { method: 'HEAD' })
133
+ if (response.ok) {
134
+ const conf = {
135
+ type: 'JB2TrackHubConnection',
136
+ uri,
137
+ name: `conn_${assemblyName}`,
138
+ assemblyNames: [assemblyName],
139
+ connectionId,
89
140
  }
90
- } finally {
91
- pending.delete(assemblyName)
141
+ session.addConnectionConf(conf)
142
+ // `silent`: no "Successfully loaded" snackbar. Nobody asked for this
143
+ // connection — it exists because a track referenced an assembly the
144
+ // session didn't have — so announcing it is noise over whatever the
145
+ // user was doing, and it lands on top of any screenshot of a hub
146
+ // genome. The property is newer than every released core (it landed
147
+ // after v4.3.0) and this plugin runs against whatever core the host
148
+ // page shipped, down to the v4.0.0 floor, but passing it is safe
149
+ // regardless: MST drops an undeclared key off a model snapshot rather
150
+ // than rejecting it (verified against the fork — the instance is
151
+ // created, the key is neither readable on it nor in its snapshot), so
152
+ // an older core makes the same connection it always did and keeps its
153
+ // toast.
154
+ session.makeConnection(conf, { silent: true })
155
+ return
92
156
  }
93
157
  }
94
158
  }
@@ -103,12 +167,12 @@ export default class HubsViewerPlugin extends Plugin {
103
167
  (defaultResult, args) => {
104
168
  const session = args.session as Session | undefined
105
169
  const assemblyName = args.assemblyName as string | undefined
106
- const uri = session && assemblyName && getConfigUrl(assemblyName)
107
- if (session && assemblyName && uri) {
170
+ const uris = assemblyName ? getConfigUrls(assemblyName) : []
171
+ if (session && assemblyName && uris.length > 0) {
108
172
  // the extension point is sync; the probe and the connection it may
109
173
  // create land later, which is fine because assemblyManager re-reads
110
174
  // reactively once the assembly shows up
111
- connectIfConfigExists(session, assemblyName, uri).catch(
175
+ connectIfConfigExists(session, assemblyName, uris).catch(
112
176
  (e: unknown) => {
113
177
  console.error(e)
114
178
  },