@jbrowse/product-core 5.0.0-beta.1 → 5.0.0-beta.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.
Files changed (53) hide show
  1. package/esm/RootModel/BaseRootModel.js +2 -1
  2. package/esm/RootModel/PreferencesConfig.d.ts +15 -0
  3. package/esm/RootModel/PreferencesConfig.js +15 -0
  4. package/esm/RootModel/createConfigModel.d.ts +8 -0
  5. package/esm/Session/BaseSession.d.ts +11 -26
  6. package/esm/Session/BaseSession.js +21 -60
  7. package/esm/Session/MultipleViews.d.ts +17 -3
  8. package/esm/Session/MultipleViews.js +45 -5
  9. package/esm/Session/Preferences.d.ts +2 -1
  10. package/esm/Session/Preferences.js +5 -0
  11. package/esm/Session/SessionTracks.d.ts +4 -1
  12. package/esm/Session/SessionTracks.js +8 -0
  13. package/esm/Session/TrackMenu.js +2 -2
  14. package/esm/Session/Tracks.d.ts +4 -5
  15. package/esm/Session/Tracks.js +115 -92
  16. package/esm/Session/developerMode.d.ts +7 -0
  17. package/esm/Session/developerMode.js +43 -0
  18. package/esm/Session/readdedTrackConf.d.ts +8 -0
  19. package/esm/Session/readdedTrackConf.js +25 -0
  20. package/esm/Session/temporaryAssemblyTracks.d.ts +4 -3
  21. package/esm/Session/temporaryAssemblyTracks.js +6 -7
  22. package/esm/controllerTracks.d.ts +3 -3
  23. package/esm/controllerTracks.js +17 -4
  24. package/esm/index.d.ts +2 -2
  25. package/esm/index.js +2 -2
  26. package/esm/loadPlugins.d.ts +1 -1
  27. package/esm/loadPlugins.js +6 -3
  28. package/esm/pruneUnbuildableNodes.js +5 -2
  29. package/esm/rpcWorker.js +5 -4
  30. package/esm/ui/AboutDialogContents.js +2 -1
  31. package/esm/ui/DescriptionPanel.d.ts +3 -0
  32. package/esm/ui/DescriptionPanel.js +24 -0
  33. package/esm/ui/DisplayDefaultsSection.d.ts +7 -20
  34. package/esm/ui/DisplayDefaultsSection.js +9 -47
  35. package/esm/ui/GeneralPreferencesTab.d.ts +14 -0
  36. package/esm/ui/GeneralPreferencesTab.js +27 -0
  37. package/esm/ui/PreferenceCheckbox.d.ts +6 -0
  38. package/esm/ui/PreferenceCheckbox.js +16 -0
  39. package/esm/ui/PreferencesDialog.d.ts +6 -27
  40. package/esm/ui/PreferencesDialog.js +38 -119
  41. package/esm/ui/ViewPreferencesTab.d.ts +12 -0
  42. package/esm/ui/ViewPreferencesTab.js +15 -0
  43. package/esm/ui/descriptionHtml.d.ts +26 -0
  44. package/esm/ui/descriptionHtml.js +66 -0
  45. package/esm/ui/displayDefaultChanges.d.ts +31 -0
  46. package/esm/ui/displayDefaultChanges.js +66 -0
  47. package/esm/ui/preferencesReset.d.ts +18 -0
  48. package/esm/ui/preferencesReset.js +64 -0
  49. package/esm/useEngineLifecycle.d.ts +7 -1
  50. package/esm/useEngineLifecycle.js +24 -2
  51. package/package.json +5 -4
  52. package/esm/scheduleDetachedDestroy.d.ts +0 -18
  53. package/esm/scheduleDetachedDestroy.js +0 -24
@@ -1,8 +1,10 @@
1
- import { types } from '@jbrowse/mobx-state-tree';
2
- import { computed } from 'mobx';
1
+ import { expandLooseTrackConfig } from '@jbrowse/core/util/tracks';
2
+ import { addDisposer, types } from '@jbrowse/mobx-state-tree';
3
+ import { autorun, computed } from 'mobx';
3
4
  import { BaseSessionModel, isBaseSession } from './BaseSession.js';
4
5
  import { isSessionWithConnections } from './Connections.js';
5
6
  import { ReferenceManagementSessionMixin } from './ReferenceManagement.js';
7
+ import { assertNotReaddedDifferently } from './readdedTrackConf.js';
6
8
  import { assertTrackConfOutlivesItsAssemblies } from './temporaryAssemblyTracks.js';
7
9
  /**
8
10
  * #stateModel TracksManagerSessionMixin
@@ -22,8 +24,11 @@ export function TracksManagerSessionMixin(pluginManager) {
22
24
  // One index trackId → config for all tracks, assembly sequences, and
23
25
  // connection tracks. Frozen jbrowse.tracks entries stay plain objects
24
26
  // here; hydration to MST nodes happens lazily in TrackConfigurationReference
25
- // on first access. Cached, so the N per-id lookups below share one rebuild
26
- // per change rather than each re-scanning.
27
+ // on first access. Held hot by the autorun in afterAttach, so a reader
28
+ // outside any reaction (ranking search hits: 33ms per search unheld on a
29
+ // 2000-track config, 0.07ms held) gets the cache too. Not `keepAlive`: that
30
+ // subscription never ends, and it reaches jbrowse.tracks on the root, so
31
+ // it pinned every superseded session for the tab's life.
27
32
  const tracksByIdRecord = computed(() => {
28
33
  const temporaryAssemblies = 'temporaryAssemblies' in self
29
34
  ? self.temporaryAssemblies
@@ -49,7 +54,7 @@ export function TracksManagerSessionMixin(pluginManager) {
49
54
  e.config,
50
55
  ]),
51
56
  ]);
52
- });
57
+ }, { name: 'tracksByIdRecord' });
53
58
  // Per-id computed cache backing getTrackById. Resolving one track's config
54
59
  // subscribes only to that id's computed, so editing track A leaves track
55
60
  // B's observers untouched: an unedited id's entry keeps its object identity
@@ -92,96 +97,114 @@ export function TracksManagerSessionMixin(pluginManager) {
92
97
  };
93
98
  })
94
99
  .actions(self => ({
95
- /**
96
- * #action
97
- * Add a track config to *this session*.
98
- *
99
- * This mixin's session has no separate session-track store, so the
100
- * destination is the jbrowse config — which in the products that compose
101
- * it (desktop) is the single user's own file rather than something a
102
- * server hands other visitors, so the two scopes are the same place.
103
- * Defined here anyway so that every session has it: a feature standing a
104
- * track up on the user's behalf can then call one action everywhere
105
- * instead of asking which mixin it landed on.
106
- * `SessionTracksManagerSessionMixin` overrides it with the real
107
- * session-scoped store.
108
- */
109
- addSessionTrackConf(trackConf) {
110
- assertTrackConfOutlivesItsAssemblies(self, trackConf, 'jbrowse.tracks');
111
- return self.jbrowse.addTrackConf(trackConf);
100
+ afterAttach() {
101
+ addDisposer(self, autorun(() => {
102
+ self.getTracksById();
103
+ }));
112
104
  },
113
- /**
114
- * #action
115
- * Add a track config wherever *this user's* catalog edits belong — the
116
- * "Add track" workflows, where an admin adding a track means to add it
117
- * for the whole site. `SessionTracksManagerSessionMixin` overrides it to
118
- * send a non-admin's to the session instead; here there is only the one
119
- * destination.
120
- *
121
- * Anything that is not an Add-track workflow wants
122
- * `addSessionTrackConf`: a track a feature stands up on the user's behalf
123
- * — a search result, a computed consensus, a reconstruction's labels — is
124
- * not a catalog entry, and publishing one writes it into the config.json
125
- * every visitor is served, once per click.
126
- */
127
- publishTrackConf(trackConf) {
128
- assertTrackConfOutlivesItsAssemblies(self, trackConf, 'jbrowse.tracks');
129
- return self.jbrowse.addTrackConf(trackConf);
130
- },
131
- /**
132
- * #action
133
- * Deprecated alias of `addSessionTrackConf`. Call that, or
134
- * `publishTrackConf`, which say which destination they mean.
135
- *
136
- * @deprecated
137
- *
138
- * Kept because a prebuilt plugin bundle reaches this by name at runtime
139
- * and cannot be recompiled — `jbrowse-plugin-protein3d` calls it, and a
140
- * member lookup that stops resolving throws nothing at all
141
- * (`pluginFacingSessionApi.test.ts` is the guard). It now means the
142
- * session, which is the destination every such caller wanted: a track a
143
- * plugin stands up for one user is not a catalog entry. Nothing in tree
144
- * may call it — `no-restricted-syntax` says so.
145
- *
146
- * Spelled out rather than delegating through `this`, for the reason
147
- * `SessionTracks.addToSession` is a plain closure: an action whose return
148
- * type is inferred from a sibling's makes the pair circular.
149
- */
150
- addTrackConf(trackConf) {
105
+ }))
106
+ .actions(self => {
107
+ function addToSession(trackConf) {
151
108
  assertTrackConfOutlivesItsAssemblies(self, trackConf, 'jbrowse.tracks');
152
- return self.jbrowse.addTrackConf(trackConf);
153
- },
154
- /**
155
- * #action
156
- * Persist edited track config back to the in-memory jbrowse config. The
157
- * session-tracks mixin overrides this so a non-admin's edits become a
158
- * shareable session-track override instead.
159
- */
160
- updateTrackConfiguration(trackConf) {
161
- // an opened connection track lives in connectionTrackConfigs, not
162
- // jbrowse.tracks; persist its edit there (jbrowse.updateTrackConf would
163
- // no-op, since the track isn't in the config). Desktop uses this base
164
- // mixin, so without this branch a connection-track edit is lost on
165
- // reload.
166
- if (isSessionWithConnections(self) &&
167
- trackConf.trackId in self.connectionTrackConfigs) {
168
- self.updateConnectionTrackConfig(trackConf);
169
- }
170
- else {
171
- self.jbrowse.updateTrackConf(trackConf);
172
- }
173
- },
174
- /**
175
- * #action
176
- */
177
- deleteTrackConf(trackConf) {
178
- const { trackId } = trackConf;
179
- self.dereferenceTrack(trackId, self.getReferring(trackId));
180
- if (self.adminMode) {
181
- return self.jbrowse.deleteTrackConf(trackConf);
109
+ const expanded = expandLooseTrackConfig(trackConf, pluginManager);
110
+ const { trackId, type } = expanded;
111
+ const existing = self.getTrackById(trackId);
112
+ if (existing) {
113
+ assertNotReaddedDifferently(pluginManager, existing, {
114
+ ...expanded,
115
+ trackId,
116
+ type,
117
+ });
118
+ return existing;
182
119
  }
183
- },
184
- }));
120
+ return self.jbrowse.addTrackConf(trackConf);
121
+ }
122
+ return {
123
+ /**
124
+ * #action
125
+ * Add a track config to *this session*.
126
+ *
127
+ * This mixin's session has no separate session-track store, so the
128
+ * destination is the jbrowse config — which in the products that compose
129
+ * it (desktop) is the single user's own file rather than something a
130
+ * server hands other visitors, so the two scopes are the same place.
131
+ * Defined here anyway so that every session has it: a feature standing a
132
+ * track up on the user's behalf can then call one action everywhere
133
+ * instead of asking which mixin it landed on.
134
+ * `SessionTracksManagerSessionMixin` overrides it with the real
135
+ * session-scoped store.
136
+ */
137
+ addSessionTrackConf(trackConf) {
138
+ return addToSession(trackConf);
139
+ },
140
+ /**
141
+ * #action
142
+ * Add a track config wherever *this user's* catalog edits belong — the
143
+ * "Add track" workflows, where an admin adding a track means to add it
144
+ * for the whole site. `SessionTracksManagerSessionMixin` overrides it to
145
+ * send a non-admin's to the session instead; here there is only the one
146
+ * destination.
147
+ *
148
+ * Anything that is not an Add-track workflow wants
149
+ * `addSessionTrackConf`: a track a feature stands up on the user's behalf
150
+ * — a search result, a computed consensus, a reconstruction's labels — is
151
+ * not a catalog entry, and publishing one writes it into the config.json
152
+ * every visitor is served, once per click.
153
+ */
154
+ publishTrackConf(trackConf) {
155
+ assertTrackConfOutlivesItsAssemblies(self, trackConf, 'jbrowse.tracks');
156
+ return self.jbrowse.addTrackConf(trackConf);
157
+ },
158
+ /**
159
+ * #action
160
+ * Deprecated alias of `addSessionTrackConf`. Call that, or
161
+ * `publishTrackConf`, which say which destination they mean.
162
+ *
163
+ * @deprecated
164
+ *
165
+ * Kept because a prebuilt plugin bundle reaches this by name at runtime
166
+ * and cannot be recompiled — `jbrowse-plugin-protein3d` calls it, and a
167
+ * member lookup that stops resolving throws nothing at all
168
+ * (`pluginFacingSessionApi.test.ts` is the guard). It now means the
169
+ * session, which is the destination every such caller wanted: a track a
170
+ * plugin stands up for one user is not a catalog entry. Nothing in tree
171
+ * may call it — `no-restricted-syntax` says so.
172
+ */
173
+ addTrackConf(trackConf) {
174
+ return addToSession(trackConf);
175
+ },
176
+ /**
177
+ * #action
178
+ * Persist edited track config back to the in-memory jbrowse config. The
179
+ * session-tracks mixin overrides this so a non-admin's edits become a
180
+ * shareable session-track override instead.
181
+ */
182
+ updateTrackConfiguration(trackConf) {
183
+ // an opened connection track lives in connectionTrackConfigs, not
184
+ // jbrowse.tracks; persist its edit there (jbrowse.updateTrackConf would
185
+ // no-op, since the track isn't in the config). Desktop uses this base
186
+ // mixin, so without this branch a connection-track edit is lost on
187
+ // reload.
188
+ if (isSessionWithConnections(self) &&
189
+ trackConf.trackId in self.connectionTrackConfigs) {
190
+ self.updateConnectionTrackConfig(trackConf);
191
+ }
192
+ else {
193
+ self.jbrowse.updateTrackConf(trackConf);
194
+ }
195
+ },
196
+ /**
197
+ * #action
198
+ */
199
+ deleteTrackConf(trackConf) {
200
+ const { trackId } = trackConf;
201
+ self.dereferenceTrack(trackId, self.getReferring(trackId));
202
+ if (self.adminMode) {
203
+ return self.jbrowse.deleteTrackConf(trackConf);
204
+ }
205
+ },
206
+ };
207
+ });
185
208
  }
186
209
  /** Type guard for SessionWithTracks */
187
210
  export function isSessionWithTracks(thing) {
@@ -0,0 +1,7 @@
1
+ import type { NotificationSink } from '@jbrowse/core/util/types';
2
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
3
+ interface DeveloperModeHost extends IStateTreeNode, Pick<NotificationSink, 'notify'> {
4
+ getPreference: (key: string) => unknown;
5
+ }
6
+ export declare function applyDeveloperMode(self: DeveloperModeHost): void;
7
+ export {};
@@ -0,0 +1,43 @@
1
+ import { addDisposer, isAlive } from '@jbrowse/mobx-state-tree';
2
+ import { enableContractReports, setContractReportSink, } from '@jbrowse/render-core/contractReports';
3
+ /**
4
+ * What a plugin author sees when their display breaks an ordering contract in
5
+ * somebody else's production build.
6
+ *
7
+ * The checks themselves have always run and always reported; what they had no
8
+ * way to reach was this population, which is the one least able to diagnose the
9
+ * symptom and the one nobody can write a test for. A `console.error` surviving
10
+ * the build would not have fixed that — nobody has a console open on a page
11
+ * they did not write — so the report has to land in the app, in the session the
12
+ * broken display is in, and it has to explain itself: the reader did not turn
13
+ * this on deliberately in the common case (a plugin served from their own
14
+ * machine did), and a message they cannot place reads as the app being broken.
15
+ *
16
+ * `warning` rather than `error`: an error snackbar in this app means the user's
17
+ * data or their request failed, and every word here is about code they are
18
+ * writing. Warnings persist until dismissed, which is what a message with a fix
19
+ * in it wants, and identical ones collapse in the snackbar — so a violation
20
+ * repeating across ten tracks of one broken type is still one notice.
21
+ */
22
+ function notification({ family, message, armedBy }) {
23
+ return (`JBrowse ${family} contract broken — a bug in code, not in your data or ` +
24
+ `configuration.\n\n${message}\n\n` +
25
+ `Shown because ${armedBy}. These notices can be turned on anywhere with ` +
26
+ `localStorage.jbrowseDeveloperMode = 1, or site-wide with ` +
27
+ `configuration.preferences.developerMode.`);
28
+ }
29
+ export function applyDeveloperMode(self) {
30
+ if (self.getPreference('developerMode') === true) {
31
+ enableContractReports('this site sets configuration.preferences.developerMode');
32
+ }
33
+ addDisposer(self, setContractReportSink(report => {
34
+ // out of whatever reaction, fetch handler or upload found the violation:
35
+ // `notify` writes observables, and a diagnostic that throws where it is
36
+ // read is worse than the bug it describes
37
+ queueMicrotask(() => {
38
+ if (isAlive(self)) {
39
+ self.notify(notification(report), 'warning');
40
+ }
41
+ });
42
+ }));
43
+ }
@@ -0,0 +1,8 @@
1
+ import type PluginManager from '@jbrowse/core/PluginManager';
2
+ import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
3
+ type TrackConfInput = {
4
+ trackId: string;
5
+ type: string;
6
+ } & Record<string, unknown>;
7
+ export declare function assertNotReaddedDifferently(pluginManager: PluginManager, existing: AnyConfigurationModel | TrackConfInput, trackConf: TrackConfInput): void;
8
+ export {};
@@ -0,0 +1,25 @@
1
+ import { getSnapshot, isStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ import { compareStructural } from 'mobx';
3
+ // What a config means once its defaults are resolved: a live config node
4
+ // answers with its snapshot, and a plain object (desktop's `jbrowse.tracks` is
5
+ // frozen, so its entries are the input as written) is hydrated through the
6
+ // same schema first. Two inputs that would build the same node compare equal.
7
+ function resolvedTrackConf(pluginManager, conf) {
8
+ return isStateTreeNode(conf)
9
+ ? getSnapshot(conf)
10
+ : getSnapshot(pluginManager
11
+ .pluggableConfigSchemaType('track')
12
+ .create(conf, { pluginManager }));
13
+ }
14
+ // Re-adding a trackId the session already holds returns the existing config,
15
+ // which is what keeps a content-hashed trackId idempotent. It is also the trap
16
+ // a recomputed track walks into: the new features arrive under the old id, the
17
+ // old config is handed back, nothing is said, and the track keeps showing the
18
+ // first values it saw. Same id with different content is therefore refused,
19
+ // naming the two-step that works.
20
+ export function assertNotReaddedDifferently(pluginManager, existing, trackConf) {
21
+ const same = compareStructural(resolvedTrackConf(pluginManager, existing), resolvedTrackConf(pluginManager, trackConf));
22
+ if (!same) {
23
+ throw new Error(`Track "${trackConf.trackId}" is already in this session with a different configuration, and adding it again would keep the old one. Delete it first (session.deleteTrackConf(session.getTrackById("${trackConf.trackId}"))), then add — and give a recomputed FromConfigAdapter a new adapterId, since the adapter cache is keyed on it.`);
24
+ }
25
+ }
@@ -1,7 +1,7 @@
1
1
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
2
  /**
3
- * Dev-only check that a track config being written into a session or config list
4
- * is one something can still draw tomorrow. No-op in production.
3
+ * Checks that a track config being written into a session or config list is one
4
+ * something can still draw tomorrow.
5
5
  *
6
6
  * A config naming nothing but a temporary assembly is dead the moment its view
7
7
  * closes, and no list outside that view has anyone to sweep it — which is the
@@ -18,6 +18,7 @@ import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
18
18
  * `console.error` and never `throw`, matching `assertDisplayContract`: these
19
19
  * writes happen inside launchers and menu handlers where an exception is caught
20
20
  * and reported as a failed track, which would hide the violation. The jest gate
21
- * (`config/jest/contractGate.js`) is what listens.
21
+ * (`config/jest/contractGate.js`) is what listens in tree; out of tree
22
+ * `contractReports` does, once a host arms it.
22
23
  */
23
24
  export declare function assertTrackConfOutlivesItsAssemblies(session: unknown, trackConf: AnyConfigurationModel | Record<string, unknown>, destination: string): void;
@@ -1,7 +1,8 @@
1
1
  import { namesTemporaryAssembly } from '@jbrowse/core/util';
2
+ import { reportContractViolation } from '@jbrowse/render-core/contractReports';
2
3
  /**
3
- * Dev-only check that a track config being written into a session or config list
4
- * is one something can still draw tomorrow. No-op in production.
4
+ * Checks that a track config being written into a session or config list is one
5
+ * something can still draw tomorrow.
5
6
  *
6
7
  * A config naming nothing but a temporary assembly is dead the moment its view
7
8
  * closes, and no list outside that view has anyone to sweep it — which is the
@@ -18,17 +19,15 @@ import { namesTemporaryAssembly } from '@jbrowse/core/util';
18
19
  * `console.error` and never `throw`, matching `assertDisplayContract`: these
19
20
  * writes happen inside launchers and menu handlers where an exception is caught
20
21
  * and reported as a failed track, which would hide the violation. The jest gate
21
- * (`config/jest/contractGate.js`) is what listens.
22
+ * (`config/jest/contractGate.js`) is what listens in tree; out of tree
23
+ * `contractReports` does, once a host arms it.
22
24
  */
23
25
  export function assertTrackConfOutlivesItsAssemblies(session, trackConf, destination) {
24
- if (process.env.NODE_ENV === 'production') {
25
- return;
26
- }
27
26
  if (!namesTemporaryAssembly(session, trackConf)) {
28
27
  return;
29
28
  }
30
29
  const { trackId } = trackConf;
31
- console.error(`[jbrowse session contract] ${destination} was given "${trackId}", ` +
30
+ reportContractViolation('session', `${destination} was given "${trackId}", ` +
32
31
  `which names a temporary assembly — one a comparative view synthesized ` +
33
32
  `and gives back when it closes, so this config outlives the only ` +
34
33
  `assembly that could draw it and nothing sweeps the list it landed in. ` +
@@ -14,7 +14,7 @@ interface ControllerView {
14
14
  trackId: string;
15
15
  };
16
16
  }[];
17
- showTrack: (trackId: string) => unknown;
17
+ launchTrack: (trackId: string) => Promise<unknown>;
18
18
  hideTrack: (trackId: string) => unknown;
19
19
  }
20
20
  export interface ControllerSession extends IStateTreeNode {
@@ -48,9 +48,9 @@ export declare function resolveTracks(tracks: TrackInput[], node: IStateTreeNode
48
48
  * Session-scoped: these are the host's declared tracks for one embed, not a
49
49
  * catalog an admin curates.
50
50
  */
51
- export declare function openTracks(session: ControllerSession, tracks: TrackConf[]): void;
51
+ export declare function openTracks(session: ControllerSession, tracks: TrackConf[]): Promise<void>;
52
52
  /** Open every wanted track and close any others the view currently shows. */
53
- export declare function reconcileTracks(session: ControllerSession, tracks: TrackConf[]): void;
53
+ export declare function reconcileTracks(session: ControllerSession, tracks: TrackConf[]): Promise<void>;
54
54
  /** Merge a hub's own search adapters with the ones the caller supplied. */
55
55
  export declare function mergeSearchAdapters<T>(a: readonly T[] | undefined, b: readonly T[] | undefined): T[] | undefined;
56
56
  export {};
@@ -1,5 +1,6 @@
1
1
  import { getEnv } from '@jbrowse/core/util';
2
2
  import { guessTrackConf } from '@jbrowse/core/util/tracks';
3
+ import { isAlive } from '@jbrowse/mobx-state-tree';
3
4
  import { resolveLocalFileUris } from './localFiles.js';
4
5
  export function isLooseTrack(track) {
5
6
  return typeof track === 'string' || (!('adapter' in track) && 'uri' in track);
@@ -47,20 +48,32 @@ export function resolveTracks(tracks, node, assemblyName, localFiles) {
47
48
  * Session-scoped: these are the host's declared tracks for one embed, not a
48
49
  * catalog an admin curates.
49
50
  */
50
- export function openTracks(session, tracks) {
51
+ export async function openTracks(session, tracks) {
52
+ // sequential, not Promise.all: each display's state model may be a separate
53
+ // dynamic import, and racing them would land the tracks on the view in
54
+ // whatever order the chunks happen to resolve rather than the caller's
51
55
  for (const conf of tracks) {
56
+ // the engine can be destroyed while a chunk is in flight (an unmounted
57
+ // embed); reading the dead tree from this floating promise would throw
58
+ // uncatchably
59
+ if (!isAlive(session.view)) {
60
+ return;
61
+ }
52
62
  const trackId = conf.trackId;
53
63
  if (!session.getTrackById(trackId)) {
54
64
  session.addSessionTrackConf(conf);
55
65
  }
56
- session.view.showTrack(trackId);
66
+ await session.view.launchTrack(trackId);
57
67
  }
58
68
  }
59
69
  /** Open every wanted track and close any others the view currently shows. */
60
- export function reconcileTracks(session, tracks) {
70
+ export async function reconcileTracks(session, tracks) {
61
71
  const { view } = session;
62
72
  const wanted = new Set(tracks.map(t => t.trackId));
63
- openTracks(session, tracks);
73
+ await openTracks(session, tracks);
74
+ if (!isAlive(view)) {
75
+ return;
76
+ }
64
77
  // materialize the ids first: hideTrack splices view.tracks, so iterating it
65
78
  // live would skip entries
66
79
  const unwanted = view.tracks
package/esm/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { AssertExtends, AssertNotAny } from './assertExtends.ts';
2
2
  export { asRoot, asSession } from './siblingCast.ts';
3
- export { scheduleDetachedDestroy } from './scheduleDetachedDestroy.ts';
3
+ export { scheduleDetachedDestroy } from '@jbrowse/core/util';
4
4
  export { BaseRootModelFactory, FormatAboutConfigSchemaFactory, FormatDetailsConfigSchemaFactory, HierarchicalConfigSchemaFactory, InternetAccountsRootModelMixin, PreferencesConfigSchemaFactory, createConfigModel, exportSessionMenuItem, importSessionMenuItem, isRootModel, newSessionMenuItem, openConnectionMenuItem, openTrackMenuItem, pluginStoreMenuItem, preferencesMenuItem, redoMenuItem, undoMenuItem, workspacesMenuItem, } from './RootModel/index.ts';
5
5
  export type { BaseRootModel, BaseRootModelType, RootConfigurationSnapshot, RootModelWithInternetAccounts, RootModelWithInternetAccountsType, } from './RootModel/index.ts';
6
6
  export { BaseSessionModel, ConnectionManagementSessionMixin, DrawerWidgetSessionMixin, MultipleViewsSessionMixin, PreferencesSessionMixin, ReferenceManagementSessionMixin, SessionTracksManagerSessionMixin, ThemeManagerSessionMixin, TrackMenuItemsSessionMixin, TrackMenuSessionMixin, TracksManagerSessionMixin, aboutTrackMenuItem, bakePromotedDefaultsIntoSnapshot, bakeSessionCascades, getShareableSessionSnapshot, copyTrackSnapshot, finalizeSession, isBaseSession, isSession, isSessionWithConnections, isSessionWithDrawerWidgets, isSessionWithMultipleViews, isSessionWithReferenceManagement, isSessionWithThemes, isSessionWithTracks, pluginExtraTrackItems, trackActionItems, trackActionMenuItems, trackListMenuItems, } from './Session/index.ts';
@@ -21,7 +21,7 @@ export type { ControllerSession, TrackConf, TrackInput, } from './controllerTrac
21
21
  export { resolveAssemblies, resolveAssembly } from './resolveAssemblies.ts';
22
22
  export type { AssemblyInput, ResolvedAssemblies } from './resolveAssemblies.ts';
23
23
  export { destroyViewState } from './destroyViewState.ts';
24
- export { useCreateOnce, useDestroyOnUnmount } from './useEngineLifecycle.ts';
24
+ export { useAsyncEngineLifecycle, useCreateOnce, useCreateOnceAsync, useDestroyOnUnmount, } from './useEngineLifecycle.ts';
25
25
  export { getSessionSnapshot, observeSession } from './observeSession.ts';
26
26
  export type { SessionObservers, ViewLocation } from './observeSession.ts';
27
27
  export { MIGRATED_DISPLAY_INSTANCE_KEYS, migrateConfigSnapshot, migrateSessionSnapshot, } from './sessionMigrations/index.ts';
package/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { asRoot, asSession } from './siblingCast.js';
2
- export { scheduleDetachedDestroy } from './scheduleDetachedDestroy.js';
2
+ export { scheduleDetachedDestroy } from '@jbrowse/core/util';
3
3
  export { BaseRootModelFactory, FormatAboutConfigSchemaFactory, FormatDetailsConfigSchemaFactory, HierarchicalConfigSchemaFactory, InternetAccountsRootModelMixin, PreferencesConfigSchemaFactory, createConfigModel, exportSessionMenuItem, importSessionMenuItem, isRootModel, newSessionMenuItem, openConnectionMenuItem, openTrackMenuItem, pluginStoreMenuItem, preferencesMenuItem, redoMenuItem, undoMenuItem, workspacesMenuItem, } from './RootModel/index.js';
4
4
  export { BaseSessionModel, ConnectionManagementSessionMixin, DrawerWidgetSessionMixin, MultipleViewsSessionMixin, PreferencesSessionMixin, ReferenceManagementSessionMixin, SessionTracksManagerSessionMixin, ThemeManagerSessionMixin, TrackMenuItemsSessionMixin, TrackMenuSessionMixin, TracksManagerSessionMixin, aboutTrackMenuItem, bakePromotedDefaultsIntoSnapshot, bakeSessionCascades, getShareableSessionSnapshot, copyTrackSnapshot, finalizeSession, isBaseSession, isSession, isSessionWithConnections, isSessionWithDrawerWidgets, isSessionWithMultipleViews, isSessionWithReferenceManagement, isSessionWithThemes, isSessionWithTracks, pluginExtraTrackItems, trackActionItems, trackActionMenuItems, trackListMenuItems, } from './Session/index.js';
5
5
  export { AboutDialog, Drawer, DrawerControls, DrawerHeader, DrawerWidget, DrawerWidgetSelector, ModalWidget, ModalWidgetAppBar, PreferencesDialog, WidgetBody, WidgetHeading, drawerGridTemplateColumns, } from './ui/index.js';
@@ -22,7 +22,7 @@ export { resolveAssemblies, resolveAssembly } from './resolveAssemblies.js';
22
22
  export { destroyViewState } from './destroyViewState.js';
23
23
  // the two halves of "React owns this engine", both of which have a StrictMode
24
24
  // trap in them that the obvious spelling walks straight into
25
- export { useCreateOnce, useDestroyOnUnmount } from './useEngineLifecycle.js';
25
+ export { useAsyncEngineLifecycle, useCreateOnce, useCreateOnceAsync, useDestroyOnUnmount, } from './useEngineLifecycle.js';
26
26
  // the JS -> host direction of an embedded controller: where each view is
27
27
  // looking, what got selected, and the layout as plain JSON
28
28
  export { getSessionSnapshot, observeSession } from './observeSession.js';
@@ -30,4 +30,4 @@ export interface LoadPluginsArgs {
30
30
  export declare function loadRuntimePlugins(pluginDefinitions: PluginDefinition[], { dropVendored, jbrowseVersion, ...args }: LoadPluginsArgs & {
31
31
  dropVendored: boolean;
32
32
  jbrowseVersion: string;
33
- }): Promise<import("@jbrowse/core/PluginLoader").PluginRecord[]>;
33
+ }): Promise<never[] | import("@jbrowse/core/PluginLoader").PluginRecord[]>;
@@ -28,11 +28,14 @@ export async function loadRuntimePlugins(pluginDefinitions, { dropVendored, jbro
28
28
  if (failures[0]) {
29
29
  throw failures[0].error;
30
30
  }
31
- const pluginLoader = new PluginLoader(dropVendored ? dropVendoredPlugins(definitions) : definitions, args);
32
- pluginLoader.installGlobalReExports(window);
31
+ const toLoad = dropVendored ? dropVendoredPlugins(definitions) : definitions;
33
32
  // the default matters: `new URL('umd_plugin.js', undefined)` throws, so a
34
33
  // relative plugin url used to fail outright instead of resolving against the
35
34
  // page the way every other relative url does
36
35
  const base = args.baseUri ?? args.baseUrl;
37
- return pluginLoader.load(base ?? window.location.href);
36
+ return toLoad.length
37
+ ? new PluginLoader(toLoad, args)
38
+ .installGlobalReExports(window)
39
+ .load(base ?? window.location.href)
40
+ : [];
38
41
  }
@@ -12,8 +12,11 @@ const HELD_NODES_KEY = 'heldForMissingPlugins';
12
12
  // widen the plugin ABI for nothing.
13
13
  function buildableElements(pluginManager, group) {
14
14
  return pluginManager.getElementTypesInGroup(group).filter(t => {
15
- const { stateModel } = t;
16
- return isType(stateModel) && isModelType(stateModel);
15
+ const element = t;
16
+ if (element.isStateModelLoaded === false) {
17
+ return false;
18
+ }
19
+ return isType(element.stateModel) && isModelType(element.stateModel);
17
20
  });
18
21
  }
19
22
  // optional/stripDefault/late/snapshotProcessor all report the type they wrap,
package/esm/rpcWorker.js CHANGED
@@ -3,7 +3,6 @@ import PluginManager from '@jbrowse/core/PluginManager';
3
3
  import { setNumberGrouping, throttleStatusEmits } from '@jbrowse/core/util';
4
4
  import { RpcServer, serializeError } from '@jbrowse/core/util/librpc';
5
5
  import { setStackTraceLimit } from '@jbrowse/core/util/setStackTraceLimit';
6
- import { enableStaticRendering } from 'mobx-react';
7
6
  // waits for a message from the main thread containing our configuration, which
8
7
  // must be sent on boot
9
8
  function receiveConfiguration() {
@@ -25,13 +24,16 @@ async function getPluginManager(corePlugins, opts) {
25
24
  // tooltip against the full feature here rather than shipping it back — so it
26
25
  // needs the main thread's display preference before any RPC method runs
27
26
  setNumberGrouping(config.numberGrouping);
28
- const pluginLoader = new PluginLoader(config.plugins, opts).installGlobalReExports(self);
29
27
  // Keep each runtime plugin's `definition` on its load record (mirroring the
30
28
  // main thread's createPluginManager) so PluginManager populates
31
29
  // runtimePluginDefinitions. A plugin that resolves a sibling asset from its
32
30
  // own bundle url in the worker — e.g. GraphGenomeView's Bandage layout
33
31
  // engine — needs its definition here, not just its instance.
34
- const runtimePlugins = await pluginLoader.load(config.windowHref);
32
+ const runtimePlugins = config.plugins.length
33
+ ? await new PluginLoader(config.plugins, opts)
34
+ .installGlobalReExports(self)
35
+ .load(config.windowHref)
36
+ : [];
35
37
  return new PluginManager([
36
38
  ...corePlugins.map(P => ({ plugin: new P() })),
37
39
  ...runtimePlugins.map(({ plugin: P, definition }) => ({
@@ -92,7 +94,6 @@ export async function initializeWorker(corePlugins, opts) {
92
94
  // copies meant a fifth product would silently ship without it — the plugins
93
95
  // that could trip it are not loaded until getPluginManager below, so this is
94
96
  // still ahead of anything that observes.
95
- enableStaticRendering(true);
96
97
  // Add global error handler to catch uncaught errors in the worker
97
98
  self.addEventListener('error', event => {
98
99
  console.error('[Worker uncaught error]', event.error || event.message);
@@ -9,6 +9,7 @@ import { makeStyles } from '@jbrowse/core/util/tss-react';
9
9
  import { isStateTreeNode } from '@jbrowse/mobx-state-tree';
10
10
  import { observer } from 'mobx-react';
11
11
  import AssemblyInfoPanel from './AssemblyInfoPanel.js';
12
+ import DescriptionPanel from './DescriptionPanel.js';
12
13
  import FileInfoPanel from './FileInfoPanel.js';
13
14
  import HeaderButtons from './HeaderButtons.js';
14
15
  import RefNameInfoDialog from './RefNameInfoDialog.js';
@@ -49,7 +50,7 @@ const AboutDialogContents = observer(function AboutDialogContents({ config, sess
49
50
  session,
50
51
  pluginManager,
51
52
  });
52
- return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: conf, hideUris: hideUris, fromDisplayTypeDefaults: fromDisplayTypeDefaults, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: shown.metadata, hideUris: hideUris }) })) : null, _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
53
+ return (_jsxs("div", { className: classes.content, children: [_jsxs(BaseCard, { title: "Configuration", children: [_jsx(HeaderButtons, { conf: conf, hideUris: hideUris, fromDisplayTypeDefaults: fromDisplayTypeDefaults, setShowRefNames: setShowRefNames }), _jsx(Attributes, { attributes: shown, omit: [...hideFields, 'metadata'], hideUris: hideUris })] }), shown.metadata ? (_jsx(BaseCard, { title: "Metadata", children: _jsx(Attributes, { attributes: shown.metadata, hideUris: hideUris }) })) : null, _jsx(DescriptionPanel, { config: config, session: session }), _jsx(AssemblyInfoPanel, { config: config, session: session, hideUris: hideUris }), _jsx(PluggableComponents, { pluginManager: pluginManager,
53
54
  /** #extensionPoint Core-extraAboutPanel | sync | Add extra panels to a track's About dialog */
54
55
  name: "Core-extraAboutPanel", props: { session, config } }), hideUris ? null : _jsx(FileInfoPanel, { config: config, session: session }), showRefNames ? (_jsx(RefNameInfoDialog, { session: session, config: config, onClose: () => {
55
56
  setShowRefNames(false);
@@ -0,0 +1,3 @@
1
+ import type { AboutPanelProps } from './util.ts';
2
+ declare const DescriptionPanel: ({ config, }: AboutPanelProps) => import("react").JSX.Element | null;
3
+ export default DescriptionPanel;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import BaseCard from '@jbrowse/core/BaseFeatureWidget/BaseFeatureDetail/BaseCard';
3
+ import { ErrorBanner, LoadingEllipses, SanitizedHTML } from '@jbrowse/core/ui';
4
+ import { openLocation } from '@jbrowse/core/util/io';
5
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
6
+ import { useFetch } from '@jbrowse/core/util/useFetch';
7
+ import { Link, Typography } from '@mui/material';
8
+ import { observer } from 'mobx-react';
9
+ import { getDescriptionHtmlUrl, rebaseDescriptionHtml, } from './descriptionHtml.js';
10
+ const useStyles = makeStyles()(theme => ({
11
+ provenance: {
12
+ marginBottom: theme.spacing(1),
13
+ },
14
+ }));
15
+ const DescriptionCard = observer(function DescriptionCard({ url, }) {
16
+ const { classes } = useStyles();
17
+ const { data: html, error, isLoading, } = useFetch(['trackDescriptionHtml', url], async (_name, url) => rebaseDescriptionHtml(await openLocation({ uri: url, locationType: 'UriLocation' }).readFile('utf8'), url));
18
+ return (_jsxs(BaseCard, { title: "Description", children: [_jsxs(Typography, { variant: "caption", color: "textSecondary", component: "div", className: classes.provenance, children: ["From the track hub, written for the UCSC Genome Browser \u2014 some display options it describes differ in JBrowse.", ' ', _jsx(Link, { href: url, target: "_blank", rel: "noopener noreferrer", children: "View original" })] }), error ? (_jsx(ErrorBanner, { error: error })) : isLoading || html === undefined ? (_jsx(LoadingEllipses, { message: "Loading description" })) : (_jsx(SanitizedHTML, { html: html }))] }));
19
+ });
20
+ const DescriptionPanel = observer(function DescriptionPanel({ config, }) {
21
+ const url = getDescriptionHtmlUrl(config);
22
+ return url ? _jsx(DescriptionCard, { url: url }) : null;
23
+ });
24
+ export default DescriptionPanel;