@cmdcolin/jbrowse-plugin-hubs 1.0.2 → 1.0.3

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.
@@ -0,0 +1,10 @@
1
+ export declare const fetchingAssemblies: Set<string>;
2
+ export interface HubSession {
3
+ assemblyManager: {
4
+ get: (name: string) => unknown;
5
+ };
6
+ addSessionAssembly: (assembly: unknown) => void;
7
+ addTrackConf: (track: unknown) => void;
8
+ notifyError: (msg: string, e: unknown) => void;
9
+ }
10
+ export declare function fetchAssemblyHub(session: HubSession, assemblyName: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ // Track assemblies we're currently fetching to avoid duplicate requests
2
+ export const fetchingAssemblies = new Set();
3
+ function getGenArkConfigUrl(accession) {
4
+ const [base, rest] = accession.split('_');
5
+ if (!rest) {
6
+ return undefined;
7
+ }
8
+ const match = rest.match(/.{1,3}/g);
9
+ if (!match || match.length < 3) {
10
+ return undefined;
11
+ }
12
+ const [b1, b2, b3] = match;
13
+ return `https://jbrowse.org/hubs/genark/${base}/${b1}/${b2}/${b3}/${accession}/config.json`;
14
+ }
15
+ function getConfigUrl(assemblyName) {
16
+ // GenArk assemblies (GCA_* or GCF_*)
17
+ if (assemblyName.startsWith('GCA_') || assemblyName.startsWith('GCF_')) {
18
+ return getGenArkConfigUrl(assemblyName);
19
+ }
20
+ // UCSC assemblies (hg38, mm10, etc.)
21
+ return `https://jbrowse.org/ucsc/${assemblyName}/config.json`;
22
+ }
23
+ export async function fetchAssemblyHub(session, assemblyName) {
24
+ const url = getConfigUrl(assemblyName);
25
+ if (!url) {
26
+ return;
27
+ }
28
+ try {
29
+ const response = await fetch(url);
30
+ if (!response.ok) {
31
+ throw new Error(`HTTP ${response.status} from ${url}`);
32
+ }
33
+ const config = (await response.json());
34
+ // Add assemblies directly to session
35
+ for (const assembly of config.assemblies ?? []) {
36
+ if (!session.assemblyManager.get(assembly.name)) {
37
+ session.addSessionAssembly(assembly);
38
+ }
39
+ }
40
+ // Add tracks directly to session (addTrackConf handles duplicates)
41
+ for (const track of config.tracks ?? []) {
42
+ session.addTrackConf(track);
43
+ }
44
+ }
45
+ catch (e) {
46
+ console.error(e);
47
+ session.notifyError(`Failed to load assembly ${assemblyName}: ${e}`, e);
48
+ }
49
+ finally {
50
+ fetchingAssemblies.delete(assemblyName);
51
+ }
52
+ }
53
+ //# sourceMappingURL=fetchAssemblyHub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchAssemblyHub.js","sourceRoot":"","sources":["../src/fetchAssemblyHub.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAU,CAAA;AASnD,SAAS,kBAAkB,CAAC,SAAiB;IAC3C,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACzC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IACnC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,CAAA;IAC1B,OAAO,mCAAmC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,SAAS,cAAc,CAAA;AAC7F,CAAC;AAED,SAAS,YAAY,CAAC,YAAoB;IACxC,qCAAqC;IACrC,IAAI,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACvE,OAAO,kBAAkB,CAAC,YAAY,CAAC,CAAA;IACzC,CAAC;IACD,qCAAqC;IACrC,OAAO,4BAA4B,YAAY,cAAc,CAAA;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAmB,EACnB,YAAoB;IAEpB,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACtC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAM;IACR,CAAC;IACD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAA;QACjC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAA;QACxD,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAGpC,CAAA;QAED,qCAAqC;QACrC,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;YAC/C,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChD,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;YACtC,CAAC;QACH,CAAC;QAED,mEAAmE;QACnE,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;YACxC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;QAC7B,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAChB,OAAO,CAAC,WAAW,CAAC,2BAA2B,YAAY,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IACzE,CAAC;YAAS,CAAC;QACT,kBAAkB,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACzC,CAAC;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,203 @@
1
+ import assert from 'node:assert';
2
+ import { beforeEach, describe, it } from 'node:test';
3
+ import { fetchAssemblyHub, fetchingAssemblies } from "./fetchAssemblyHub.js";
4
+ function createMockSession() {
5
+ const calls = {
6
+ addSessionAssembly: [],
7
+ addTrackConf: [],
8
+ notifyError: [],
9
+ };
10
+ return {
11
+ session: {
12
+ assemblyManager: {
13
+ get: (_name) => undefined,
14
+ },
15
+ addSessionAssembly: (assembly) => {
16
+ calls.addSessionAssembly.push(assembly);
17
+ },
18
+ addTrackConf: (track) => {
19
+ calls.addTrackConf.push(track);
20
+ },
21
+ notifyError: (msg, e) => {
22
+ calls.notifyError.push({ msg, e });
23
+ },
24
+ },
25
+ calls,
26
+ };
27
+ }
28
+ function mockFetch(config, ok = true, status = 200) {
29
+ const originalFetch = globalThis.fetch;
30
+ globalThis.fetch = (async () => ({
31
+ ok,
32
+ status,
33
+ json: async () => config,
34
+ }));
35
+ return () => {
36
+ globalThis.fetch = originalFetch;
37
+ };
38
+ }
39
+ describe('fetchAssemblyHub', () => {
40
+ beforeEach(() => {
41
+ fetchingAssemblies.clear();
42
+ });
43
+ it('adds assemblies from fetched config', async () => {
44
+ const { session, calls } = createMockSession();
45
+ const mockConfig = {
46
+ assemblies: [{ name: 'hg38', sequence: {} }],
47
+ tracks: [],
48
+ };
49
+ const restore = mockFetch(mockConfig);
50
+ fetchingAssemblies.add('hg38');
51
+ await fetchAssemblyHub(session, 'hg38');
52
+ restore();
53
+ assert.strictEqual(calls.addSessionAssembly.length, 1);
54
+ assert.deepStrictEqual(calls.addSessionAssembly[0], {
55
+ name: 'hg38',
56
+ sequence: {},
57
+ });
58
+ assert.strictEqual(fetchingAssemblies.has('hg38'), false);
59
+ });
60
+ it('adds tracks from fetched config', async () => {
61
+ const { session, calls } = createMockSession();
62
+ const mockConfig = {
63
+ assemblies: [],
64
+ tracks: [{ trackId: 'genes', type: 'FeatureTrack' }],
65
+ };
66
+ const restore = mockFetch(mockConfig);
67
+ fetchingAssemblies.add('hg38');
68
+ await fetchAssemblyHub(session, 'hg38');
69
+ restore();
70
+ assert.strictEqual(calls.addTrackConf.length, 1);
71
+ assert.deepStrictEqual(calls.addTrackConf[0], {
72
+ trackId: 'genes',
73
+ type: 'FeatureTrack',
74
+ });
75
+ });
76
+ it('skips assembly if already exists', async () => {
77
+ const { session, calls } = createMockSession();
78
+ session.assemblyManager.get = () => ({ name: 'hg38' });
79
+ const mockConfig = {
80
+ assemblies: [{ name: 'hg38', sequence: {} }],
81
+ tracks: [],
82
+ };
83
+ const restore = mockFetch(mockConfig);
84
+ fetchingAssemblies.add('hg38');
85
+ await fetchAssemblyHub(session, 'hg38');
86
+ restore();
87
+ assert.strictEqual(calls.addSessionAssembly.length, 0);
88
+ });
89
+ it('handles fetch error gracefully', async () => {
90
+ const { session, calls } = createMockSession();
91
+ const restore = mockFetch({}, false, 404);
92
+ fetchingAssemblies.add('nonexistent');
93
+ await fetchAssemblyHub(session, 'nonexistent');
94
+ restore();
95
+ assert.strictEqual(calls.notifyError.length, 1);
96
+ assert.strictEqual(fetchingAssemblies.has('nonexistent'), false);
97
+ });
98
+ it('handles network error gracefully', async () => {
99
+ const { session, calls } = createMockSession();
100
+ const originalFetch = globalThis.fetch;
101
+ globalThis.fetch = (async () => {
102
+ throw new Error('Network error');
103
+ });
104
+ fetchingAssemblies.add('hg38');
105
+ await fetchAssemblyHub(session, 'hg38');
106
+ globalThis.fetch = originalFetch;
107
+ assert.strictEqual(calls.notifyError.length, 1);
108
+ assert.strictEqual(fetchingAssemblies.has('hg38'), false);
109
+ });
110
+ it('cleans up fetchingAssemblies on success', async () => {
111
+ const { session } = createMockSession();
112
+ const mockConfig = { assemblies: [], tracks: [] };
113
+ const restore = mockFetch(mockConfig);
114
+ fetchingAssemblies.add('hg38');
115
+ assert.strictEqual(fetchingAssemblies.has('hg38'), true);
116
+ await fetchAssemblyHub(session, 'hg38');
117
+ restore();
118
+ assert.strictEqual(fetchingAssemblies.has('hg38'), false);
119
+ });
120
+ });
121
+ describe('extension point handler', () => {
122
+ it('fetchingAssemblies prevents duplicate requests', () => {
123
+ fetchingAssemblies.add('hg38');
124
+ const shouldFetch = !fetchingAssemblies.has('hg38');
125
+ assert.strictEqual(shouldFetch, false);
126
+ });
127
+ it('allows fetch for new assembly', () => {
128
+ fetchingAssemblies.clear();
129
+ const shouldFetch = !fetchingAssemblies.has('hg38');
130
+ assert.strictEqual(shouldFetch, true);
131
+ });
132
+ });
133
+ describe('GenArk assembly support', () => {
134
+ beforeEach(() => {
135
+ fetchingAssemblies.clear();
136
+ });
137
+ it('fetches GenArk assembly with GCF_ prefix', async () => {
138
+ const { session, calls } = createMockSession();
139
+ const mockConfig = {
140
+ assemblies: [{ name: 'GCF_009858895.2', sequence: {} }],
141
+ tracks: [],
142
+ };
143
+ let fetchedUrl = '';
144
+ const originalFetch = globalThis.fetch;
145
+ globalThis.fetch = (async (url) => {
146
+ fetchedUrl = url;
147
+ return {
148
+ ok: true,
149
+ status: 200,
150
+ json: async () => mockConfig,
151
+ };
152
+ });
153
+ fetchingAssemblies.add('GCF_009858895.2');
154
+ await fetchAssemblyHub(session, 'GCF_009858895.2');
155
+ globalThis.fetch = originalFetch;
156
+ assert.strictEqual(fetchedUrl, 'https://jbrowse.org/hubs/genark/GCF/009/858/895/GCF_009858895.2/config.json');
157
+ assert.strictEqual(calls.addSessionAssembly.length, 1);
158
+ });
159
+ it('fetches GenArk assembly with GCA_ prefix', async () => {
160
+ const { session, calls } = createMockSession();
161
+ const mockConfig = {
162
+ assemblies: [{ name: 'GCA_000001405.15', sequence: {} }],
163
+ tracks: [],
164
+ };
165
+ let fetchedUrl = '';
166
+ const originalFetch = globalThis.fetch;
167
+ globalThis.fetch = (async (url) => {
168
+ fetchedUrl = url;
169
+ return {
170
+ ok: true,
171
+ status: 200,
172
+ json: async () => mockConfig,
173
+ };
174
+ });
175
+ fetchingAssemblies.add('GCA_000001405.15');
176
+ await fetchAssemblyHub(session, 'GCA_000001405.15');
177
+ globalThis.fetch = originalFetch;
178
+ assert.strictEqual(fetchedUrl, 'https://jbrowse.org/hubs/genark/GCA/000/001/405/GCA_000001405.15/config.json');
179
+ assert.strictEqual(calls.addSessionAssembly.length, 1);
180
+ });
181
+ it('uses UCSC URL for non-GenArk assemblies', async () => {
182
+ const { session } = createMockSession();
183
+ const mockConfig = {
184
+ assemblies: [{ name: 'hg38', sequence: {} }],
185
+ tracks: [],
186
+ };
187
+ let fetchedUrl = '';
188
+ const originalFetch = globalThis.fetch;
189
+ globalThis.fetch = (async (url) => {
190
+ fetchedUrl = url;
191
+ return {
192
+ ok: true,
193
+ status: 200,
194
+ json: async () => mockConfig,
195
+ };
196
+ });
197
+ fetchingAssemblies.add('hg38');
198
+ await fetchAssemblyHub(session, 'hg38');
199
+ globalThis.fetch = originalFetch;
200
+ assert.strictEqual(fetchedUrl, 'https://jbrowse.org/ucsc/hg38/config.json');
201
+ });
202
+ });
203
+ //# sourceMappingURL=fetchAssemblyHub.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchAssemblyHub.test.js","sourceRoot":"","sources":["../src/fetchAssemblyHub.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,5 +4,6 @@ export default class HubsViewerPlugin extends Plugin {
4
4
  name: string;
5
5
  version: any;
6
6
  install(pluginManager: PluginManager): void;
7
- configure(pluginManager: PluginManager): void;
7
+ configure(_pluginManager: PluginManager): void;
8
8
  }
9
+ export { type HubSession, fetchAssemblyHub, fetchingAssemblies, } from './fetchAssemblyHub.ts';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Plugin from '@jbrowse/core/Plugin';
2
+ import { fetchAssemblyHub, fetchingAssemblies } from "./fetchAssemblyHub.js";
2
3
  import { version } from '../package.json';
3
4
  export default class HubsViewerPlugin extends Plugin {
4
5
  name = 'HubsViewerPlugin';
@@ -7,22 +8,21 @@ export default class HubsViewerPlugin extends Plugin {
7
8
  pluginManager.addToExtensionPoint('Core-handleUnrecognizedAssembly', (_defaultResult, args) => {
8
9
  const session = args.session;
9
10
  const assemblyName = args.assemblyName;
10
- const jb2asm = `jb2hub-${assemblyName}`;
11
- if (assemblyName &&
12
- !session.connections.find((f) => f.connectionId === jb2asm)) {
13
- console.log('getUnrecognizedAssembly', { assemblyName });
14
- const conf = {
15
- type: 'JB2TrackHubConnection',
16
- uri: `https://jbrowse.org/ucsc/${assemblyName}/config.json`,
17
- name: `conn_${assemblyName}`,
18
- assemblyNames: [assemblyName],
19
- connectionId: jb2asm,
20
- };
21
- session.addConnectionConf(conf);
22
- session.makeConnection(conf);
11
+ // Skip if missing required args
12
+ if (!session || !assemblyName) {
13
+ return;
23
14
  }
15
+ // Skip if already fetching (must check this first to prevent recursion)
16
+ if (fetchingAssemblies.has(assemblyName)) {
17
+ return;
18
+ }
19
+ // Mark as fetching before any other checks that might re-trigger this
20
+ fetchingAssemblies.add(assemblyName);
21
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
22
+ fetchAssemblyHub(session, assemblyName);
24
23
  });
25
24
  }
26
- configure(pluginManager) { }
25
+ configure(_pluginManager) { }
27
26
  }
27
+ export { fetchAssemblyHub, fetchingAssemblies, } from "./fetchAssemblyHub.js";
28
28
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,sBAAsB,CAAA;AAGzC,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAEzC,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,MAAM;IAClD,IAAI,GAAG,kBAAkB,CAAA;IACzB,OAAO,GAAG,OAAO,CAAA;IAEjB,OAAO,CAAC,aAA4B;QAClC,aAAa,CAAC,mBAAmB,CAC/B,iCAAiC,EACjC,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE;YACvB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAc,CAAA;YACnC,MAAM,YAAY,GAAG,IAAI,CAAC,YAAsB,CAAA;YAChD,MAAM,MAAM,GAAG,UAAU,YAAY,EAAE,CAAA;YACvC,IACE,YAAY;gBACZ,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CACvB,CAAC,CAA2B,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,MAAM,CAC3D,EACD,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,EAAE,YAAY,EAAE,CAAC,CAAA;gBACxD,MAAM,IAAI,GAAG;oBACX,IAAI,EAAE,uBAAuB;oBAC7B,GAAG,EAAE,4BAA4B,YAAY,cAAc;oBAC3D,IAAI,EAAE,QAAQ,YAAY,EAAE;oBAC5B,aAAa,EAAE,CAAC,YAAY,CAAC;oBAC7B,YAAY,EAAE,MAAM;iBACrB,CAAA;gBACD,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAA;gBAC/B,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAA;YAC9B,CAAC;QACH,CAAC,CACF,CAAA;IACH,CAAC;IAED,SAAS,CAAC,aAA4B,IAAG,CAAC;CAC3C"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,sBAAsB,CAAA;AAGzC,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAIzC,MAAM,CAAC,OAAO,OAAO,gBAAiB,SAAQ,MAAM;IAClD,IAAI,GAAG,kBAAkB,CAAA;IACzB,OAAO,GAAG,OAAO,CAAA;IAEjB,OAAO,CAAC,aAA4B;QAClC,aAAa,CAAC,mBAAmB,CAC/B,iCAAiC,EACjC,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE;YACvB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAiC,CAAA;YACtD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAkC,CAAA;YAE5D,gCAAgC;YAChC,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC;gBAC9B,OAAM;YACR,CAAC;YAED,wEAAwE;YACxE,IAAI,kBAAkB,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;gBACzC,OAAM;YACR,CAAC;YAED,sEAAsE;YACtE,kBAAkB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YAEpC,mEAAmE;YACnE,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;QACzC,CAAC,CACF,CAAA;IACH,CAAC;IAED,SAAS,CAAC,cAA6B,IAAG,CAAC;CAC5C;AAED,OAAO,EAEL,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,uBAAuB,CAAA"}
@@ -1,2 +1,2 @@
1
- "use strict";var JBrowsePluginHubs=(()=>{var g=Object.create;var a=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty;var w=(n,e)=>()=>(e||n((e={exports:{}}).exports,e),e.exports),x=(n,e)=>{for(var s in e)a(n,s,{get:e[s],enumerable:!0})},d=(n,e,s,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of y(e))!h.call(n,i)&&i!==s&&a(n,i,{get:()=>e[i],enumerable:!(r=f(e,i))||r.enumerable});return n};var v=(n,e,s)=>(s=n!=null?g(j(n)):{},d(e||!n||!n.__esModule?a(s,"default",{value:n,enumerable:!0}):s,n)),k=n=>d(a({},"__esModule",{value:!0}),n);var p=w((N,m)=>{m.exports=JBrowseExports["@jbrowse/core/Plugin"]});var P={};x(P,{default:()=>l});var b=v(p());var u="1.0.2";var l=class extends b.default{name="HubsViewerPlugin";version=u;install(e){e.addToExtensionPoint("Core-handleUnrecognizedAssembly",(s,r)=>{let i=r.session,t=r.assemblyName,c=`jb2hub-${t}`;if(t&&!i.connections.find(o=>o.connectionId===c)){console.log("getUnrecognizedAssembly",{assemblyName:t});let o={type:"JB2TrackHubConnection",uri:`https://jbrowse.org/ucsc/${t}/config.json`,name:`conn_${t}`,assemblyNames:[t],connectionId:c};i.addConnectionConf(o),i.makeConnection(o)}})}configure(e){}};return k(P);})();
1
+ "use strict";var JBrowsePluginHubs=(()=>{var g=Object.create;var a=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,x=Object.prototype.hasOwnProperty;var j=(e,s)=>()=>(s||e((s={exports:{}}).exports,s),s.exports),k=(e,s)=>{for(var n in s)a(e,n,{get:s[n],enumerable:!0})},u=(e,s,n,t)=>{if(s&&typeof s=="object"||typeof s=="function")for(let r of h(s))!x.call(e,r)&&r!==n&&a(e,r,{get:()=>s[r],enumerable:!(t=y(s,r))||t.enumerable});return e};var A=(e,s,n)=>(n=e!=null?g(w(e)):{},u(s||!e||!e.__esModule?a(n,"default",{value:e,enumerable:!0}):n,e)),v=e=>u(a({},"__esModule",{value:!0}),e);var m=j((M,d)=>{d.exports=JBrowseExports["@jbrowse/core/Plugin"]});var S={};k(S,{default:()=>c,fetchAssemblyHub:()=>l,fetchingAssemblies:()=>o});var f=A(m());var o=new Set;function H(e){let[s,n]=e.split("_");if(!n)return;let t=n.match(/.{1,3}/g);if(!t||t.length<3)return;let[r,i,p]=t;return`https://jbrowse.org/hubs/genark/${s}/${r}/${i}/${p}/${e}/config.json`}function $(e){return e.startsWith("GCA_")||e.startsWith("GCF_")?H(e):`https://jbrowse.org/ucsc/${e}/config.json`}async function l(e,s){let n=$(s);if(n)try{let t=await fetch(n);if(!t.ok)throw new Error(`HTTP ${t.status} from ${n}`);let r=await t.json();for(let i of r.assemblies??[])e.assemblyManager.get(i.name)||e.addSessionAssembly(i);for(let i of r.tracks??[])e.addTrackConf(i)}catch(t){console.error(t),e.notifyError(`Failed to load assembly ${s}: ${t}`,t)}finally{o.delete(s)}}var b="1.0.3";var c=class extends f.default{name="HubsViewerPlugin";version=b;install(s){s.addToExtensionPoint("Core-handleUnrecognizedAssembly",(n,t)=>{let r=t.session,i=t.assemblyName;!r||!i||o.has(i)||(o.add(i),l(r,i))})}configure(s){}};return v(S);})();
2
2
  //# sourceMappingURL=jbrowse-plugin-hubs.umd.production.min.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["global-externals:@jbrowse/core/Plugin", "../src/index.ts", "../package.json"],
4
- "sourcesContent": ["module.exports = JBrowseExports[\"@jbrowse/core/Plugin\"];", "import Plugin from '@jbrowse/core/Plugin'\nimport PluginManager from '@jbrowse/core/PluginManager'\n\nimport { version } from '../package.json'\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 any\n const assemblyName = args.assemblyName as string\n const jb2asm = `jb2hub-${assemblyName}`\n if (\n assemblyName &&\n !session.connections.find(\n (f: { connectionId: string }) => f.connectionId === jb2asm,\n )\n ) {\n console.log('getUnrecognizedAssembly', { assemblyName })\n const conf = {\n type: 'JB2TrackHubConnection',\n uri: `https://jbrowse.org/ucsc/${assemblyName}/config.json`,\n name: `conn_${assemblyName}`,\n assemblyNames: [assemblyName],\n connectionId: jb2asm,\n }\n session.addConnectionConf(conf)\n session.makeConnection(conf)\n }\n },\n )\n }\n\n configure(pluginManager: PluginManager) {}\n}\n", "{\n \"version\": \"1.0.2\",\n \"license\": \"MIT\",\n \"name\": \"@cmdcolin/jbrowse-plugin-hubs\",\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\",\n \"test\": \"vitest\",\n \"format\": \"prettier --write .\",\n \"prebuild\": \"npm run clean\",\n \"build\": \"tsc && NODE_ENV=production node esbuild.mjs\",\n \"lint\": \"eslint --report-unused-disable-directives --max-warnings 0\",\n \"prepack\": \"npm run build\",\n \"postversion\": \"git push --follow-tags\"\n },\n \"devDependencies\": {\n \"@babel/core\": \"^7.16.5\",\n \"@babel/preset-react\": \"^7.10.4\",\n \"@emotion/react\": \"^11.10.4\",\n \"@fal-works/esbuild-plugin-global-externals\": \"^2.1.2\",\n \"@jbrowse/core\": \"^3.0.1\",\n \"@jbrowse/plugin-data-management\": \"^3.0.1\",\n \"@jbrowse/plugin-linear-genome-view\": \"^3.0.1\",\n \"@mui/material\": \"^7.0.1\",\n \"@mui/system\": \"^7.0.1\",\n \"@mui/x-data-grid\": \"^8.2.0\",\n \"@types/d3-array\": \"^3.2.1\",\n \"@types/d3-hierarchy\": \"^3.1.7\",\n \"@types/node\": \"^22.15.16\",\n \"@types/react\": \"^19.0.1\",\n \"chalk\": \"^5.3.0\",\n \"esbuild\": \"^0.25.0\",\n \"eslint\": \"^9.17.0\",\n \"eslint-plugin-import\": \"^2.31.0\",\n \"eslint-plugin-react\": \"^7.20.3\",\n \"eslint-plugin-react-hooks\": \"^5.1.0\",\n \"eslint-plugin-unicorn\": \"^60.0.0\",\n \"mobx\": \"^6.0.0\",\n \"mobx-react\": \"^9.0.1\",\n \"mobx-state-tree\": \"^5.4.1\",\n \"prettier\": \"^3.4.2\",\n \"pretty-bytes\": \"^7.0.0\",\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"rimraf\": \"^6.0.1\",\n \"rxjs\": \"^7.8.1\",\n \"serve\": \"^14.2.0\",\n \"tss-react\": \"^4.9.18\",\n \"typescript\": \"^5.1.6\",\n \"typescript-eslint\": \"^8.18.0\",\n \"vitest\": \"^3.2.1\"\n },\n \"dependencies\": {\n \"@gmod/bgzf-filehandle\": \"^4.0.0\",\n \"abortable-promise-cache\": \"^1.5.0\",\n \"buffer\": \"^6.0.3\",\n \"d3-array\": \"^3.2.4\",\n \"d3-hierarchy\": \"^3.1.2\",\n \"fast-deep-equal\": \"^3.1.3\",\n \"generic-filehandle2\": \"^2.0.1\",\n \"long\": \"^5.2.3\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n"],
5
- "mappings": "gpBAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAAAA,EAAO,QAAU,eAAe,sBAAsB,ICAtD,IAAAC,EAAA,GAAAC,EAAAD,EAAA,aAAAE,IAAA,IAAAC,EAAmB,OCCjB,IAAAC,EAAW,QDIb,IAAqBC,EAArB,cAA8C,EAAAC,OAAO,CACnD,KAAO,mBACP,QAAUC,EAEV,QAAQC,EAA8B,CACpCA,EAAc,oBACZ,kCACA,CAACC,EAAgBC,IAAS,CACxB,IAAMC,EAAUD,EAAK,QACfE,EAAeF,EAAK,aACpBG,EAAS,UAAUD,CAAY,GACrC,GACEA,GACA,CAACD,EAAQ,YAAY,KAClBG,GAAgCA,EAAE,eAAiBD,CACtD,EACA,CACA,QAAQ,IAAI,0BAA2B,CAAE,aAAAD,CAAa,CAAC,EACvD,IAAMG,EAAO,CACX,KAAM,wBACN,IAAK,4BAA4BH,CAAY,eAC7C,KAAM,QAAQA,CAAY,GAC1B,cAAe,CAACA,CAAY,EAC5B,aAAcC,CAChB,EACAF,EAAQ,kBAAkBI,CAAI,EAC9BJ,EAAQ,eAAeI,CAAI,CAC7B,CACF,CACF,CACF,CAEA,UAAUP,EAA8B,CAAC,CAC3C",
6
- "names": ["require_Plugin", "__commonJSMin", "exports", "module", "index_exports", "__export", "HubsViewerPlugin", "import_Plugin", "version", "HubsViewerPlugin", "Plugin", "version", "pluginManager", "_defaultResult", "args", "session", "assemblyName", "jb2asm", "f", "conf"]
3
+ "sources": ["global-externals:@jbrowse/core/Plugin", "../src/index.ts", "../src/fetchAssemblyHub.ts", "../package.json"],
4
+ "sourcesContent": ["module.exports = JBrowseExports[\"@jbrowse/core/Plugin\"];", "import Plugin from '@jbrowse/core/Plugin'\nimport PluginManager from '@jbrowse/core/PluginManager'\n\nimport { fetchAssemblyHub, fetchingAssemblies } from './fetchAssemblyHub.ts'\nimport { version } from '../package.json'\n\nimport type { HubSession } from './fetchAssemblyHub.ts'\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 HubSession | undefined\n const assemblyName = args.assemblyName as string | undefined\n\n // Skip if missing required args\n if (!session || !assemblyName) {\n return\n }\n\n // Skip if already fetching (must check this first to prevent recursion)\n if (fetchingAssemblies.has(assemblyName)) {\n return\n }\n\n // Mark as fetching before any other checks that might re-trigger this\n fetchingAssemblies.add(assemblyName)\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n fetchAssemblyHub(session, assemblyName)\n },\n )\n }\n\n configure(_pluginManager: PluginManager) {}\n}\n\nexport {\n type HubSession,\n fetchAssemblyHub,\n fetchingAssemblies,\n} from './fetchAssemblyHub.ts'\n", "// Track assemblies we're currently fetching to avoid duplicate requests\nexport const fetchingAssemblies = new Set<string>()\n\nexport interface HubSession {\n assemblyManager: { get: (name: string) => unknown }\n addSessionAssembly: (assembly: unknown) => void\n addTrackConf: (track: unknown) => void\n notifyError: (msg: string, e: unknown) => void\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 // GenArk assemblies (GCA_* or GCF_*)\n if (assemblyName.startsWith('GCA_') || assemblyName.startsWith('GCF_')) {\n return getGenArkConfigUrl(assemblyName)\n }\n // UCSC assemblies (hg38, mm10, etc.)\n return `https://jbrowse.org/ucsc/${assemblyName}/config.json`\n}\n\nexport async function fetchAssemblyHub(\n session: HubSession,\n assemblyName: string,\n) {\n const url = getConfigUrl(assemblyName)\n if (!url) {\n return\n }\n try {\n const response = await fetch(url)\n if (!response.ok) {\n throw new Error(`HTTP ${response.status} from ${url}`)\n }\n const config = (await response.json()) as {\n assemblies?: { name: string }[]\n tracks?: unknown[]\n }\n\n // Add assemblies directly to session\n for (const assembly of config.assemblies ?? []) {\n if (!session.assemblyManager.get(assembly.name)) {\n session.addSessionAssembly(assembly)\n }\n }\n\n // Add tracks directly to session (addTrackConf handles duplicates)\n for (const track of config.tracks ?? []) {\n session.addTrackConf(track)\n }\n } catch (e) {\n console.error(e)\n session.notifyError(`Failed to load assembly ${assemblyName}: ${e}`, e)\n } finally {\n fetchingAssemblies.delete(assemblyName)\n }\n}\n", "{\n \"version\": \"1.0.3\",\n \"license\": \"MIT\",\n \"name\": \"@cmdcolin/jbrowse-plugin-hubs\",\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\": \"node --experimental-strip-types --test src/fetchAssemblyHub.test.ts\",\n \"format\": \"prettier --write .\",\n \"prebuild\": \"npm run clean\",\n \"build\": \"tsc && NODE_ENV=production node esbuild.mjs\",\n \"lint\": \"eslint --report-unused-disable-directives --max-warnings 0\",\n \"prepack\": \"npm run build\",\n \"postversion\": \"git push --follow-tags\"\n },\n \"devDependencies\": {\n \"@babel/core\": \"^7.28.6\",\n \"@babel/preset-react\": \"^7.10.4\",\n \"@emotion/react\": \"^11.10.4\",\n \"@emotion/styled\": \"^11.14.1\",\n \"@eslint/js\": \"^9.39.2\",\n \"@fal-works/esbuild-plugin-global-externals\": \"^2.1.2\",\n \"@jbrowse/core\": \"^4.1.3\",\n \"@jbrowse/plugin-data-management\": \"^4.1.3\",\n \"@jbrowse/plugin-linear-genome-view\": \"^4.1.3\",\n \"@mui/material\": \"^7.3.7\",\n \"@mui/system\": \"^7.3.7\",\n \"@mui/x-data-grid\": \"^8.26.0\",\n \"@types/d3-array\": \"^3.2.1\",\n \"@types/d3-hierarchy\": \"^3.1.7\",\n \"@types/node\": \"^25.1.0\",\n \"@types/react\": \"^19.2.10\",\n \"esbuild\": \"^0.27.2\",\n \"eslint\": \"^9.39.2\",\n \"eslint-plugin-import\": \"^2.31.0\",\n \"eslint-plugin-unicorn\": \"^62.0.0\",\n \"mobx\": \"^6.0.0\",\n \"mobx-react\": \"^9.0.1\",\n \"mobx-state-tree\": \"^7.0.2\",\n \"prettier\": \"^3.8.1\",\n \"pretty-bytes\": \"^7.0.0\",\n \"react\": \"^19.2.4\",\n \"react-dom\": \"^19.2.4\",\n \"rimraf\": \"^6.1.2\",\n \"rxjs\": \"^7.8.1\",\n \"serve\": \"^14.2.0\",\n \"typescript\": \"^5.1.6\",\n \"typescript-eslint\": \"^8.54.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n"],
5
+ "mappings": "gpBAAA,IAAAA,EAAAC,EAAA,CAAAC,EAAAC,IAAA,CAAAA,EAAO,QAAU,eAAe,sBAAsB,ICAtD,IAAAC,EAAA,GAAAC,EAAAD,EAAA,aAAAE,EAAA,qBAAAC,EAAA,uBAAAC,IAAA,IAAAC,EAAmB,OCCZ,IAAMC,EAAqB,IAAI,IAStC,SAASC,EAAmBC,EAAmB,CAC7C,GAAM,CAACC,EAAMC,CAAI,EAAIF,EAAU,MAAM,GAAG,EACxC,GAAI,CAACE,EACH,OAEF,IAAMC,EAAQD,EAAK,MAAM,SAAS,EAClC,GAAI,CAACC,GAASA,EAAM,OAAS,EAC3B,OAEF,GAAM,CAACC,EAAIC,EAAIC,CAAE,EAAIH,EACrB,MAAO,mCAAmCF,CAAI,IAAIG,CAAE,IAAIC,CAAE,IAAIC,CAAE,IAAIN,CAAS,cAC/E,CAEA,SAASO,EAAaC,EAAsB,CAE1C,OAAIA,EAAa,WAAW,MAAM,GAAKA,EAAa,WAAW,MAAM,EAC5DT,EAAmBS,CAAY,EAGjC,4BAA4BA,CAAY,cACjD,CAEA,eAAsBC,EACpBC,EACAF,EACA,CACA,IAAMG,EAAMJ,EAAaC,CAAY,EACrC,GAAKG,EAGL,GAAI,CACF,IAAMC,EAAW,MAAM,MAAMD,CAAG,EAChC,GAAI,CAACC,EAAS,GACZ,MAAM,IAAI,MAAM,QAAQA,EAAS,MAAM,SAASD,CAAG,EAAE,EAEvD,IAAME,EAAU,MAAMD,EAAS,KAAK,EAMpC,QAAWE,KAAYD,EAAO,YAAc,CAAC,EACtCH,EAAQ,gBAAgB,IAAII,EAAS,IAAI,GAC5CJ,EAAQ,mBAAmBI,CAAQ,EAKvC,QAAWC,KAASF,EAAO,QAAU,CAAC,EACpCH,EAAQ,aAAaK,CAAK,CAE9B,OAASC,EAAG,CACV,QAAQ,MAAMA,CAAC,EACfN,EAAQ,YAAY,2BAA2BF,CAAY,KAAKQ,CAAC,GAAIA,CAAC,CACxE,QAAE,CACAlB,EAAmB,OAAOU,CAAY,CACxC,CACF,CClEE,IAAAS,EAAW,QFOb,IAAqBC,EAArB,cAA8C,EAAAC,OAAO,CACnD,KAAO,mBACP,QAAUC,EAEV,QAAQC,EAA8B,CACpCA,EAAc,oBACZ,kCACA,CAACC,EAAgBC,IAAS,CACxB,IAAMC,EAAUD,EAAK,QACfE,EAAeF,EAAK,aAGtB,CAACC,GAAW,CAACC,GAKbC,EAAmB,IAAID,CAAY,IAKvCC,EAAmB,IAAID,CAAY,EAGnCE,EAAiBH,EAASC,CAAY,EACxC,CACF,CACF,CAEA,UAAUG,EAA+B,CAAC,CAC5C",
6
+ "names": ["require_Plugin", "__commonJSMin", "exports", "module", "index_exports", "__export", "HubsViewerPlugin", "fetchAssemblyHub", "fetchingAssemblies", "import_Plugin", "fetchingAssemblies", "getGenArkConfigUrl", "accession", "base", "rest", "match", "b1", "b2", "b3", "getConfigUrl", "assemblyName", "fetchAssemblyHub", "session", "url", "response", "config", "assembly", "track", "e", "version", "HubsViewerPlugin", "Plugin", "version", "pluginManager", "_defaultResult", "args", "session", "assemblyName", "fetchingAssemblies", "fetchAssemblyHub", "_pluginManager"]
7
7
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.2",
2
+ "version": "1.0.3",
3
3
  "license": "MIT",
4
4
  "name": "@cmdcolin/jbrowse-plugin-hubs",
5
5
  "keywords": [
@@ -15,8 +15,8 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "clean": "rimraf dist",
18
- "start": "node esbuild.mjs",
19
- "test": "vitest",
18
+ "start": "node esbuild.mjs --watch",
19
+ "test": "node --experimental-strip-types --test src/fetchAssemblyHub.test.ts",
20
20
  "format": "prettier --write .",
21
21
  "prebuild": "npm run clean",
22
22
  "build": "tsc && NODE_ENV=production node esbuild.mjs",
@@ -25,51 +25,38 @@
25
25
  "postversion": "git push --follow-tags"
26
26
  },
27
27
  "devDependencies": {
28
- "@babel/core": "^7.16.5",
28
+ "@babel/core": "^7.28.6",
29
29
  "@babel/preset-react": "^7.10.4",
30
30
  "@emotion/react": "^11.10.4",
31
+ "@emotion/styled": "^11.14.1",
32
+ "@eslint/js": "^9.39.2",
31
33
  "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
32
- "@jbrowse/core": "^3.0.1",
33
- "@jbrowse/plugin-data-management": "^3.0.1",
34
- "@jbrowse/plugin-linear-genome-view": "^3.0.1",
35
- "@mui/material": "^7.0.1",
36
- "@mui/system": "^7.0.1",
37
- "@mui/x-data-grid": "^8.2.0",
34
+ "@jbrowse/core": "^4.1.3",
35
+ "@jbrowse/plugin-data-management": "^4.1.3",
36
+ "@jbrowse/plugin-linear-genome-view": "^4.1.3",
37
+ "@mui/material": "^7.3.7",
38
+ "@mui/system": "^7.3.7",
39
+ "@mui/x-data-grid": "^8.26.0",
38
40
  "@types/d3-array": "^3.2.1",
39
41
  "@types/d3-hierarchy": "^3.1.7",
40
- "@types/node": "^22.15.16",
41
- "@types/react": "^19.0.1",
42
- "chalk": "^5.3.0",
43
- "esbuild": "^0.25.0",
44
- "eslint": "^9.17.0",
42
+ "@types/node": "^25.1.0",
43
+ "@types/react": "^19.2.10",
44
+ "esbuild": "^0.27.2",
45
+ "eslint": "^9.39.2",
45
46
  "eslint-plugin-import": "^2.31.0",
46
- "eslint-plugin-react": "^7.20.3",
47
- "eslint-plugin-react-hooks": "^5.1.0",
48
- "eslint-plugin-unicorn": "^60.0.0",
47
+ "eslint-plugin-unicorn": "^62.0.0",
49
48
  "mobx": "^6.0.0",
50
49
  "mobx-react": "^9.0.1",
51
- "mobx-state-tree": "^5.4.1",
52
- "prettier": "^3.4.2",
50
+ "mobx-state-tree": "^7.0.2",
51
+ "prettier": "^3.8.1",
53
52
  "pretty-bytes": "^7.0.0",
54
- "react": "^19.0.0",
55
- "react-dom": "^19.0.0",
56
- "rimraf": "^6.0.1",
53
+ "react": "^19.2.4",
54
+ "react-dom": "^19.2.4",
55
+ "rimraf": "^6.1.2",
57
56
  "rxjs": "^7.8.1",
58
57
  "serve": "^14.2.0",
59
- "tss-react": "^4.9.18",
60
58
  "typescript": "^5.1.6",
61
- "typescript-eslint": "^8.18.0",
62
- "vitest": "^3.2.1"
63
- },
64
- "dependencies": {
65
- "@gmod/bgzf-filehandle": "^4.0.0",
66
- "abortable-promise-cache": "^1.5.0",
67
- "buffer": "^6.0.3",
68
- "d3-array": "^3.2.4",
69
- "d3-hierarchy": "^3.1.2",
70
- "fast-deep-equal": "^3.1.3",
71
- "generic-filehandle2": "^2.0.1",
72
- "long": "^5.2.3"
59
+ "typescript-eslint": "^8.54.0"
73
60
  },
74
61
  "publishConfig": {
75
62
  "access": "public"
@@ -0,0 +1,255 @@
1
+ import assert from 'node:assert'
2
+ import { beforeEach, describe, it } from 'node:test'
3
+
4
+ import { fetchAssemblyHub, fetchingAssemblies } from './fetchAssemblyHub.ts'
5
+
6
+ import type { HubSession } from './fetchAssemblyHub.ts'
7
+
8
+ function createMockSession(): {
9
+ session: HubSession
10
+ calls: {
11
+ addSessionAssembly: unknown[]
12
+ addTrackConf: unknown[]
13
+ notifyError: unknown[]
14
+ }
15
+ } {
16
+ const calls = {
17
+ addSessionAssembly: [] as unknown[],
18
+ addTrackConf: [] as unknown[],
19
+ notifyError: [] as unknown[],
20
+ }
21
+ return {
22
+ session: {
23
+ assemblyManager: {
24
+ get: (_name: string) => undefined as unknown,
25
+ },
26
+ addSessionAssembly: (assembly: unknown) => {
27
+ calls.addSessionAssembly.push(assembly)
28
+ },
29
+ addTrackConf: (track: unknown) => {
30
+ calls.addTrackConf.push(track)
31
+ },
32
+ notifyError: (msg: string, e: unknown) => {
33
+ calls.notifyError.push({ msg, e })
34
+ },
35
+ },
36
+ calls,
37
+ }
38
+ }
39
+
40
+ function mockFetch(config: unknown, ok = true, status = 200) {
41
+ const originalFetch = globalThis.fetch
42
+ globalThis.fetch = (async () => ({
43
+ ok,
44
+ status,
45
+ json: async () => config,
46
+ })) as unknown as typeof fetch
47
+ return () => {
48
+ globalThis.fetch = originalFetch
49
+ }
50
+ }
51
+
52
+ describe('fetchAssemblyHub', () => {
53
+ beforeEach(() => {
54
+ fetchingAssemblies.clear()
55
+ })
56
+
57
+ it('adds assemblies from fetched config', async () => {
58
+ const { session, calls } = createMockSession()
59
+ const mockConfig = {
60
+ assemblies: [{ name: 'hg38', sequence: {} }],
61
+ tracks: [],
62
+ }
63
+ const restore = mockFetch(mockConfig)
64
+
65
+ fetchingAssemblies.add('hg38')
66
+ await fetchAssemblyHub(session, 'hg38')
67
+ restore()
68
+
69
+ assert.strictEqual(calls.addSessionAssembly.length, 1)
70
+ assert.deepStrictEqual(calls.addSessionAssembly[0], {
71
+ name: 'hg38',
72
+ sequence: {},
73
+ })
74
+ assert.strictEqual(fetchingAssemblies.has('hg38'), false)
75
+ })
76
+
77
+ it('adds tracks from fetched config', async () => {
78
+ const { session, calls } = createMockSession()
79
+ const mockConfig = {
80
+ assemblies: [],
81
+ tracks: [{ trackId: 'genes', type: 'FeatureTrack' }],
82
+ }
83
+ const restore = mockFetch(mockConfig)
84
+
85
+ fetchingAssemblies.add('hg38')
86
+ await fetchAssemblyHub(session, 'hg38')
87
+ restore()
88
+
89
+ assert.strictEqual(calls.addTrackConf.length, 1)
90
+ assert.deepStrictEqual(calls.addTrackConf[0], {
91
+ trackId: 'genes',
92
+ type: 'FeatureTrack',
93
+ })
94
+ })
95
+
96
+ it('skips assembly if already exists', async () => {
97
+ const { session, calls } = createMockSession()
98
+ session.assemblyManager.get = () => ({ name: 'hg38' })
99
+ const mockConfig = {
100
+ assemblies: [{ name: 'hg38', sequence: {} }],
101
+ tracks: [],
102
+ }
103
+ const restore = mockFetch(mockConfig)
104
+
105
+ fetchingAssemblies.add('hg38')
106
+ await fetchAssemblyHub(session, 'hg38')
107
+ restore()
108
+
109
+ assert.strictEqual(calls.addSessionAssembly.length, 0)
110
+ })
111
+
112
+ it('handles fetch error gracefully', async () => {
113
+ const { session, calls } = createMockSession()
114
+ const restore = mockFetch({}, false, 404)
115
+
116
+ fetchingAssemblies.add('nonexistent')
117
+ await fetchAssemblyHub(session, 'nonexistent')
118
+ restore()
119
+
120
+ assert.strictEqual(calls.notifyError.length, 1)
121
+ assert.strictEqual(fetchingAssemblies.has('nonexistent'), false)
122
+ })
123
+
124
+ it('handles network error gracefully', async () => {
125
+ const { session, calls } = createMockSession()
126
+ const originalFetch = globalThis.fetch
127
+ globalThis.fetch = (async () => {
128
+ throw new Error('Network error')
129
+ }) as unknown as typeof fetch
130
+
131
+ fetchingAssemblies.add('hg38')
132
+ await fetchAssemblyHub(session, 'hg38')
133
+ globalThis.fetch = originalFetch
134
+
135
+ assert.strictEqual(calls.notifyError.length, 1)
136
+ assert.strictEqual(fetchingAssemblies.has('hg38'), false)
137
+ })
138
+
139
+ it('cleans up fetchingAssemblies on success', async () => {
140
+ const { session } = createMockSession()
141
+ const mockConfig = { assemblies: [], tracks: [] }
142
+ const restore = mockFetch(mockConfig)
143
+
144
+ fetchingAssemblies.add('hg38')
145
+ assert.strictEqual(fetchingAssemblies.has('hg38'), true)
146
+
147
+ await fetchAssemblyHub(session, 'hg38')
148
+ restore()
149
+
150
+ assert.strictEqual(fetchingAssemblies.has('hg38'), false)
151
+ })
152
+ })
153
+
154
+ describe('extension point handler', () => {
155
+ it('fetchingAssemblies prevents duplicate requests', () => {
156
+ fetchingAssemblies.add('hg38')
157
+ const shouldFetch = !fetchingAssemblies.has('hg38')
158
+ assert.strictEqual(shouldFetch, false)
159
+ })
160
+
161
+ it('allows fetch for new assembly', () => {
162
+ fetchingAssemblies.clear()
163
+ const shouldFetch = !fetchingAssemblies.has('hg38')
164
+ assert.strictEqual(shouldFetch, true)
165
+ })
166
+ })
167
+
168
+ describe('GenArk assembly support', () => {
169
+ beforeEach(() => {
170
+ fetchingAssemblies.clear()
171
+ })
172
+
173
+ it('fetches GenArk assembly with GCF_ prefix', async () => {
174
+ const { session, calls } = createMockSession()
175
+ const mockConfig = {
176
+ assemblies: [{ name: 'GCF_009858895.2', sequence: {} }],
177
+ tracks: [],
178
+ }
179
+
180
+ let fetchedUrl = ''
181
+ const originalFetch = globalThis.fetch
182
+ globalThis.fetch = (async (url: string) => {
183
+ fetchedUrl = url
184
+ return {
185
+ ok: true,
186
+ status: 200,
187
+ json: async () => mockConfig,
188
+ }
189
+ }) as unknown as typeof fetch
190
+
191
+ fetchingAssemblies.add('GCF_009858895.2')
192
+ await fetchAssemblyHub(session, 'GCF_009858895.2')
193
+ globalThis.fetch = originalFetch
194
+
195
+ assert.strictEqual(
196
+ fetchedUrl,
197
+ 'https://jbrowse.org/hubs/genark/GCF/009/858/895/GCF_009858895.2/config.json',
198
+ )
199
+ assert.strictEqual(calls.addSessionAssembly.length, 1)
200
+ })
201
+
202
+ it('fetches GenArk assembly with GCA_ prefix', async () => {
203
+ const { session, calls } = createMockSession()
204
+ const mockConfig = {
205
+ assemblies: [{ name: 'GCA_000001405.15', sequence: {} }],
206
+ tracks: [],
207
+ }
208
+
209
+ let fetchedUrl = ''
210
+ const originalFetch = globalThis.fetch
211
+ globalThis.fetch = (async (url: string) => {
212
+ fetchedUrl = url
213
+ return {
214
+ ok: true,
215
+ status: 200,
216
+ json: async () => mockConfig,
217
+ }
218
+ }) as unknown as typeof fetch
219
+
220
+ fetchingAssemblies.add('GCA_000001405.15')
221
+ await fetchAssemblyHub(session, 'GCA_000001405.15')
222
+ globalThis.fetch = originalFetch
223
+
224
+ assert.strictEqual(
225
+ fetchedUrl,
226
+ 'https://jbrowse.org/hubs/genark/GCA/000/001/405/GCA_000001405.15/config.json',
227
+ )
228
+ assert.strictEqual(calls.addSessionAssembly.length, 1)
229
+ })
230
+
231
+ it('uses UCSC URL for non-GenArk assemblies', async () => {
232
+ const { session } = createMockSession()
233
+ const mockConfig = {
234
+ assemblies: [{ name: 'hg38', sequence: {} }],
235
+ tracks: [],
236
+ }
237
+
238
+ let fetchedUrl = ''
239
+ const originalFetch = globalThis.fetch
240
+ globalThis.fetch = (async (url: string) => {
241
+ fetchedUrl = url
242
+ return {
243
+ ok: true,
244
+ status: 200,
245
+ json: async () => mockConfig,
246
+ }
247
+ }) as unknown as typeof fetch
248
+
249
+ fetchingAssemblies.add('hg38')
250
+ await fetchAssemblyHub(session, 'hg38')
251
+ globalThis.fetch = originalFetch
252
+
253
+ assert.strictEqual(fetchedUrl, 'https://jbrowse.org/ucsc/hg38/config.json')
254
+ })
255
+ })
@@ -0,0 +1,68 @@
1
+ // Track assemblies we're currently fetching to avoid duplicate requests
2
+ export const fetchingAssemblies = new Set<string>()
3
+
4
+ export interface HubSession {
5
+ assemblyManager: { get: (name: string) => unknown }
6
+ addSessionAssembly: (assembly: unknown) => void
7
+ addTrackConf: (track: unknown) => void
8
+ notifyError: (msg: string, e: unknown) => void
9
+ }
10
+
11
+ function getGenArkConfigUrl(accession: string) {
12
+ const [base, rest] = accession.split('_')
13
+ if (!rest) {
14
+ return undefined
15
+ }
16
+ const match = rest.match(/.{1,3}/g)
17
+ if (!match || match.length < 3) {
18
+ return undefined
19
+ }
20
+ const [b1, b2, b3] = match
21
+ return `https://jbrowse.org/hubs/genark/${base}/${b1}/${b2}/${b3}/${accession}/config.json`
22
+ }
23
+
24
+ function getConfigUrl(assemblyName: string) {
25
+ // GenArk assemblies (GCA_* or GCF_*)
26
+ if (assemblyName.startsWith('GCA_') || assemblyName.startsWith('GCF_')) {
27
+ return getGenArkConfigUrl(assemblyName)
28
+ }
29
+ // UCSC assemblies (hg38, mm10, etc.)
30
+ return `https://jbrowse.org/ucsc/${assemblyName}/config.json`
31
+ }
32
+
33
+ export async function fetchAssemblyHub(
34
+ session: HubSession,
35
+ assemblyName: string,
36
+ ) {
37
+ const url = getConfigUrl(assemblyName)
38
+ if (!url) {
39
+ return
40
+ }
41
+ try {
42
+ const response = await fetch(url)
43
+ if (!response.ok) {
44
+ throw new Error(`HTTP ${response.status} from ${url}`)
45
+ }
46
+ const config = (await response.json()) as {
47
+ assemblies?: { name: string }[]
48
+ tracks?: unknown[]
49
+ }
50
+
51
+ // Add assemblies directly to session
52
+ for (const assembly of config.assemblies ?? []) {
53
+ if (!session.assemblyManager.get(assembly.name)) {
54
+ session.addSessionAssembly(assembly)
55
+ }
56
+ }
57
+
58
+ // Add tracks directly to session (addTrackConf handles duplicates)
59
+ for (const track of config.tracks ?? []) {
60
+ session.addTrackConf(track)
61
+ }
62
+ } catch (e) {
63
+ console.error(e)
64
+ session.notifyError(`Failed to load assembly ${assemblyName}: ${e}`, e)
65
+ } finally {
66
+ fetchingAssemblies.delete(assemblyName)
67
+ }
68
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import Plugin from '@jbrowse/core/Plugin'
2
2
  import PluginManager from '@jbrowse/core/PluginManager'
3
3
 
4
+ import { fetchAssemblyHub, fetchingAssemblies } from './fetchAssemblyHub.ts'
4
5
  import { version } from '../package.json'
5
6
 
7
+ import type { HubSession } from './fetchAssemblyHub.ts'
8
+
6
9
  export default class HubsViewerPlugin extends Plugin {
7
10
  name = 'HubsViewerPlugin'
8
11
  version = version
@@ -11,29 +14,33 @@ export default class HubsViewerPlugin extends Plugin {
11
14
  pluginManager.addToExtensionPoint(
12
15
  'Core-handleUnrecognizedAssembly',
13
16
  (_defaultResult, args) => {
14
- const session = args.session as any
15
- const assemblyName = args.assemblyName as string
16
- const jb2asm = `jb2hub-${assemblyName}`
17
- if (
18
- assemblyName &&
19
- !session.connections.find(
20
- (f: { connectionId: string }) => f.connectionId === jb2asm,
21
- )
22
- ) {
23
- console.log('getUnrecognizedAssembly', { assemblyName })
24
- const conf = {
25
- type: 'JB2TrackHubConnection',
26
- uri: `https://jbrowse.org/ucsc/${assemblyName}/config.json`,
27
- name: `conn_${assemblyName}`,
28
- assemblyNames: [assemblyName],
29
- connectionId: jb2asm,
30
- }
31
- session.addConnectionConf(conf)
32
- session.makeConnection(conf)
17
+ const session = args.session as HubSession | undefined
18
+ const assemblyName = args.assemblyName as string | undefined
19
+
20
+ // Skip if missing required args
21
+ if (!session || !assemblyName) {
22
+ return
23
+ }
24
+
25
+ // Skip if already fetching (must check this first to prevent recursion)
26
+ if (fetchingAssemblies.has(assemblyName)) {
27
+ return
33
28
  }
29
+
30
+ // Mark as fetching before any other checks that might re-trigger this
31
+ fetchingAssemblies.add(assemblyName)
32
+
33
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
34
+ fetchAssemblyHub(session, assemblyName)
34
35
  },
35
36
  )
36
37
  }
37
38
 
38
- configure(pluginManager: PluginManager) {}
39
+ configure(_pluginManager: PluginManager) {}
39
40
  }
41
+
42
+ export {
43
+ type HubSession,
44
+ fetchAssemblyHub,
45
+ fetchingAssemblies,
46
+ } from './fetchAssemblyHub.ts'