@jbrowse/product-core 4.3.0 → 5.0.0-beta.10
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/esm/RootModel/BaseRootModel.d.ts +514 -323
- package/esm/RootModel/BaseRootModel.js +141 -14
- package/esm/RootModel/HierarchicalConfig.d.ts +30 -20
- package/esm/RootModel/HierarchicalConfig.js +36 -0
- package/esm/RootModel/InternetAccounts.d.ts +18 -1
- package/esm/RootModel/InternetAccounts.js +120 -22
- package/esm/RootModel/PreferencesConfig.d.ts +37 -0
- package/esm/RootModel/PreferencesConfig.js +75 -0
- package/esm/RootModel/createConfigModel.d.ts +257 -0
- package/esm/RootModel/createConfigModel.js +77 -0
- package/esm/RootModel/index.d.ts +10 -5
- package/esm/RootModel/index.js +10 -5
- package/esm/RootModel/menuItems.d.ts +24 -0
- package/esm/RootModel/menuItems.js +141 -0
- package/esm/Session/BaseSession.d.ts +285 -11
- package/esm/Session/BaseSession.js +383 -15
- package/esm/Session/Connections.d.ts +115 -8
- package/esm/Session/Connections.js +196 -74
- package/esm/Session/DrawerWidgets.d.ts +145 -13
- package/esm/Session/DrawerWidgets.js +219 -49
- package/esm/Session/MultipleViews.d.ts +1067 -1177
- package/esm/Session/MultipleViews.js +395 -64
- package/esm/Session/Preferences.d.ts +1389 -0
- package/esm/Session/Preferences.js +58 -0
- package/esm/Session/ReferenceManagement.d.ts +28 -3
- package/esm/Session/ReferenceManagement.js +56 -41
- package/esm/Session/SessionTracks.d.ts +1101 -1180
- package/esm/Session/SessionTracks.js +584 -34
- package/esm/Session/Themes.d.ts +136 -2
- package/esm/Session/Themes.js +267 -21
- package/esm/Session/TrackMenu.d.ts +88 -0
- package/esm/Session/TrackMenu.js +175 -0
- package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
- package/esm/Session/TrackMenuSessionMixin.js +42 -0
- package/esm/Session/Tracks.d.ts +945 -1159
- package/esm/Session/Tracks.js +229 -30
- package/esm/Session/assertSessionModel.d.ts +21 -0
- package/esm/Session/assertSessionModel.js +1 -0
- package/esm/Session/copyTrackSnapshot.d.ts +21 -0
- package/esm/Session/copyTrackSnapshot.js +20 -0
- package/esm/Session/developerMode.d.ts +7 -0
- package/esm/Session/developerMode.js +43 -0
- package/esm/Session/finalizeSession.d.ts +15 -0
- package/esm/Session/finalizeSession.js +28 -0
- package/esm/Session/hydratedForms.d.ts +16 -0
- package/esm/Session/hydratedForms.js +13 -0
- package/esm/Session/index.d.ts +27 -10
- package/esm/Session/index.js +16 -10
- package/esm/Session/pluginExtraTrackItems.d.ts +11 -0
- package/esm/Session/pluginExtraTrackItems.js +13 -0
- package/esm/Session/readdedTrackConf.d.ts +8 -0
- package/esm/Session/readdedTrackConf.js +24 -0
- package/esm/Session/shareableSnapshot.d.ts +23 -0
- package/esm/Session/shareableSnapshot.js +66 -0
- package/esm/Session/temporaryAssemblyTracks.d.ts +24 -0
- package/esm/Session/temporaryAssemblyTracks.js +37 -0
- package/esm/assertExtends.d.ts +55 -0
- package/esm/assertExtends.js +1 -0
- package/esm/controllerTracks.d.ts +76 -0
- package/esm/controllerTracks.js +125 -0
- package/esm/destroyViewState.d.ts +21 -0
- package/esm/destroyViewState.js +29 -0
- package/esm/embeddedController.d.ts +57 -0
- package/esm/embeddedController.js +100 -0
- package/esm/index.d.ts +32 -4
- package/esm/index.js +31 -4
- package/esm/loadPlugins.d.ts +38 -0
- package/esm/loadPlugins.js +45 -0
- package/esm/localFiles.d.ts +74 -0
- package/esm/localFiles.js +156 -0
- package/esm/observeSession.d.ts +55 -0
- package/esm/observeSession.js +90 -0
- package/esm/parallelChunkLoading.d.ts +30 -0
- package/esm/parallelChunkLoading.js +71 -0
- package/esm/pluginInput.d.ts +32 -0
- package/esm/pluginInput.js +19 -0
- package/esm/pruneUnbuildableNodes.d.ts +109 -0
- package/esm/pruneUnbuildableNodes.js +562 -0
- package/esm/resolveAssemblies.d.ts +68 -0
- package/esm/resolveAssemblies.js +77 -0
- package/esm/rpcWorker.d.ts +36 -4
- package/esm/rpcWorker.js +126 -20
- package/esm/sessionMigrations/index.d.ts +15 -0
- package/esm/sessionMigrations/index.js +178 -0
- package/esm/sessionUrl.d.ts +40 -0
- package/esm/sessionUrl.js +66 -0
- package/esm/sessionUtils.d.ts +74 -0
- package/esm/sessionUtils.js +599 -0
- package/esm/siblingCast.d.ts +14 -0
- package/esm/siblingCast.js +16 -0
- package/esm/snapshotUtils.d.ts +8 -0
- package/esm/snapshotUtils.js +12 -0
- package/esm/ui/AboutDialog.d.ts +8 -3
- package/esm/ui/AboutDialog.js +14 -5
- package/esm/ui/AboutDialogContents.d.ts +2 -6
- package/esm/ui/AboutDialogContents.js +13 -17
- package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
- package/esm/ui/AssemblyInfoPanel.js +63 -0
- package/esm/ui/DescriptionPanel.d.ts +3 -0
- package/esm/ui/DescriptionPanel.js +25 -0
- package/esm/ui/Drawer.d.ts +21 -0
- package/esm/ui/Drawer.js +60 -0
- package/esm/ui/DrawerControls.d.ts +5 -0
- package/esm/ui/DrawerControls.js +28 -0
- package/esm/ui/DrawerHeader.d.ts +6 -0
- package/esm/ui/DrawerHeader.js +36 -0
- package/esm/ui/DrawerWidget.d.ts +5 -0
- package/esm/ui/DrawerWidget.js +12 -0
- package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
- package/esm/ui/DrawerWidgetSelector.js +40 -0
- package/esm/ui/FileInfoPanel.d.ts +3 -6
- package/esm/ui/FileInfoPanel.js +36 -26
- package/esm/ui/GeneralPreferencesTab.d.ts +17 -0
- package/esm/ui/GeneralPreferencesTab.js +34 -0
- package/esm/ui/HeaderButtons.d.ts +2 -1
- package/esm/ui/HeaderButtons.js +15 -17
- package/esm/ui/ModalWidget.d.ts +13 -0
- package/esm/ui/ModalWidget.js +48 -0
- package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
- package/esm/ui/ModalWidgetAppBar.js +9 -0
- package/esm/ui/PreferenceCheckbox.d.ts +6 -0
- package/esm/ui/PreferenceCheckbox.js +16 -0
- package/esm/ui/PreferencesDialog.d.ts +34 -0
- package/esm/ui/PreferencesDialog.js +55 -0
- package/esm/ui/PreferencesResetDialog.d.ts +8 -0
- package/esm/ui/PreferencesResetDialog.js +19 -0
- package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
- package/esm/ui/RefNameAliasesDialog.js +65 -0
- package/esm/ui/RefNameInfoDialog.d.ts +3 -6
- package/esm/ui/RefNameInfoDialog.js +45 -63
- package/esm/ui/ViewPreferencesTab.d.ts +12 -0
- package/esm/ui/ViewPreferencesTab.js +15 -0
- package/esm/ui/WidgetBody.d.ts +18 -0
- package/esm/ui/WidgetBody.js +22 -0
- package/esm/ui/WidgetHeading.d.ts +7 -0
- package/esm/ui/WidgetHeading.js +7 -0
- package/esm/ui/aboutTestUtils.d.ts +140 -0
- package/esm/ui/aboutTestUtils.js +25 -0
- package/esm/ui/descriptionHtml.d.ts +25 -0
- package/esm/ui/descriptionHtml.js +64 -0
- package/esm/ui/drawerLayout.d.ts +18 -0
- package/esm/ui/drawerLayout.js +22 -0
- package/esm/ui/index.d.ts +15 -1
- package/esm/ui/index.js +13 -1
- package/esm/ui/preferencesReset.d.ts +18 -0
- package/esm/ui/preferencesReset.js +69 -0
- package/esm/ui/util.d.ts +22 -7
- package/esm/ui/util.js +19 -41
- package/esm/useEngineLifecycle.d.ts +19 -0
- package/esm/useEngineLifecycle.js +44 -0
- package/package.json +15 -18
- package/esm/RootModel/FormatAbout.d.ts +0 -12
- package/esm/RootModel/FormatAbout.js +0 -15
- package/esm/RootModel/FormatDetails.d.ts +0 -24
- package/esm/RootModel/FormatDetails.js +0 -27
- package/esm/Session/DialogQueue.d.ts +0 -17
- package/esm/Session/DialogQueue.js +0 -31
|
@@ -1,41 +1,599 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { readConfObject } from '@jbrowse/core/configuration';
|
|
2
|
+
import { diffTrackConfig, flattenTrackConfigDelta, mergeTrackConfig, } from '@jbrowse/core/util';
|
|
3
|
+
import { expandLooseTrackConfig } from '@jbrowse/core/util/tracks';
|
|
4
|
+
import { applySnapshot, getSnapshot, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
|
|
5
|
+
import { compareStructural, computed } from 'mobx';
|
|
6
|
+
import { TracksManagerSessionMixin } from './Tracks.js';
|
|
7
|
+
import { hydratedTrackForm } from './hydratedForms.js';
|
|
8
|
+
import { assertNotReaddedDifferently } from './readdedTrackConf.js';
|
|
9
|
+
import { assertTrackConfOutlivesItsAssemblies } from './temporaryAssemblyTracks.js';
|
|
10
|
+
// jbrowse.tracks holds frozen plain objects in every product; single site for
|
|
11
|
+
// the cast from its loose frozen type.
|
|
12
|
+
function baseTracks(self) {
|
|
13
|
+
return self.jbrowse.tracks;
|
|
14
|
+
}
|
|
15
|
+
function sessionEntries(self) {
|
|
16
|
+
return getSnapshot(self.sessionTracks);
|
|
17
|
+
}
|
|
18
|
+
// Not a key count: every delta keeps its trackId, and one can hold nothing but
|
|
19
|
+
// content-free display stubs. A stored delta lights the edited badge, so only a
|
|
20
|
+
// changed slot — a reset included — counts.
|
|
21
|
+
function deltaHasChanges(base, delta) {
|
|
22
|
+
return flattenTrackConfigDelta(base, delta).length > 0;
|
|
23
|
+
}
|
|
24
|
+
// The assemblies a track config names that the config.json does not carry.
|
|
25
|
+
// `self.jbrowse.assemblies` is the catalog alone — the session's own
|
|
26
|
+
// `assemblies` getter adds sessionAssemblies to it, which is the opposite of
|
|
27
|
+
// what this asks.
|
|
28
|
+
//
|
|
29
|
+
// An assembly's `aliases` are names of that assembly, so a track naming one
|
|
30
|
+
// names something the catalog carries and publishes like any other. Matching on
|
|
31
|
+
// `name` alone diverted it to the session and told the admin the config.json
|
|
32
|
+
// has no such assembly while they were looking at it. Read off the configs
|
|
33
|
+
// rather than through the assembly manager: the question is what the config.json
|
|
34
|
+
// declares, and the manager also answers for sessionAssemblies, which is the
|
|
35
|
+
// case this exists to catch.
|
|
36
|
+
function assembliesNotInTheCatalog(self, trackConf) {
|
|
37
|
+
const catalog = new Set(self.jbrowse.assemblies.flatMap(a => [
|
|
38
|
+
readConfObject(a, 'name'),
|
|
39
|
+
...(readConfObject(a, 'aliases') ?? []),
|
|
40
|
+
]));
|
|
41
|
+
const names = readConfObject(trackConf, 'assemblyNames');
|
|
42
|
+
return names?.filter(name => !catalog.has(name)) ?? [];
|
|
43
|
+
}
|
|
44
|
+
function withoutDelta(deltas, trackId) {
|
|
45
|
+
const { [trackId]: _dropped, ...rest } = deltas;
|
|
46
|
+
return rest;
|
|
47
|
+
}
|
|
48
|
+
// Not evicted: bounded by the distinct ids read this session.
|
|
49
|
+
function perIdComputed(derive) {
|
|
50
|
+
const computeds = new Map();
|
|
51
|
+
return (trackId) => {
|
|
52
|
+
let c = computeds.get(trackId);
|
|
53
|
+
if (!c) {
|
|
54
|
+
c = computed(() => derive(trackId));
|
|
55
|
+
computeds.set(trackId, c);
|
|
56
|
+
}
|
|
57
|
+
return c.get();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* #stateModel SessionTracksManagerSessionMixin
|
|
62
|
+
*/
|
|
4
63
|
export function SessionTracksManagerSessionMixin(pluginManager) {
|
|
64
|
+
// A base is the raw config-file object — shorthand `uri`, no display stubs —
|
|
65
|
+
// while a working copy's snapshot is hydrated. Diffing or merging across the
|
|
66
|
+
// two reads every expanded field as an edit, pinning whole adapters into the
|
|
67
|
+
// delta, and every member one form has and the other lacks as a reset. So
|
|
68
|
+
// both sides go through the same schema first; a base is memoized per
|
|
69
|
+
// frozen-base identity, which is stable until a jbrowse.tracks write.
|
|
70
|
+
const hydrate = hydratedTrackForm(pluginManager);
|
|
71
|
+
const canonicalBaseCache = new WeakMap();
|
|
72
|
+
function toPlainConfig(base) {
|
|
73
|
+
const cached = canonicalBaseCache.get(base);
|
|
74
|
+
if (cached) {
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
const hydrated = hydrate(base);
|
|
78
|
+
canonicalBaseCache.set(base, hydrated);
|
|
79
|
+
return hydrated;
|
|
80
|
+
}
|
|
5
81
|
return TracksManagerSessionMixin(pluginManager)
|
|
6
82
|
.named('SessionTracksManagerSessionMixin')
|
|
7
83
|
.props({
|
|
8
|
-
|
|
84
|
+
/**
|
|
85
|
+
* #property
|
|
86
|
+
* Tracks the session added, each entry the base its edits
|
|
87
|
+
* (trackConfigDeltas) diff against, as a config.json entry is for a
|
|
88
|
+
* config track.
|
|
89
|
+
*/
|
|
90
|
+
sessionTracks: types.stripDefault(types.array(pluginManager.pluggableConfigSchemaType('track')), []),
|
|
91
|
+
/**
|
|
92
|
+
* #property
|
|
93
|
+
* Per-track config overrides, keyed by trackId, stored as a *delta*
|
|
94
|
+
* against the base config (the sessionTracks or jbrowse.tracks entry)
|
|
95
|
+
* rather than a full copy —
|
|
96
|
+
* so a later change to an untouched field of the base still flows through
|
|
97
|
+
* (see trackConfigDelta.ts). A `null` member resets a slot the base sets.
|
|
98
|
+
* An admin's edits land here too, and reach jbrowse.tracks only through
|
|
99
|
+
* `promoteTrackConfigDeltas`. Frozen (not a typed track array) on purpose: a typed
|
|
100
|
+
* create() would fill defaults, erasing the "unset vs default"
|
|
101
|
+
* distinction the delta merge relies on.
|
|
102
|
+
*
|
|
103
|
+
* `stripDefault` for the reason every other persisted prop beside it has
|
|
104
|
+
* it: without it the empty map is written into every snapshot and every
|
|
105
|
+
* share link, so a session that never overrode a track still ships
|
|
106
|
+
* `"trackConfigDeltas":{}`. It was the one prop added since that convention
|
|
107
|
+
* without it.
|
|
108
|
+
*/
|
|
109
|
+
trackConfigDeltas: types.stripDefault(types.frozen(), {}),
|
|
110
|
+
})
|
|
111
|
+
.volatile(() => ({
|
|
112
|
+
/**
|
|
113
|
+
* Per-track private working copies, keyed by trackId. A plain
|
|
114
|
+
* Map — not observable, not persisted — mirroring the pluginManager
|
|
115
|
+
* hydration cache: it holds the live MST config node a shown track's
|
|
116
|
+
* in-place quick-edits mutate, so the shared frozen base is never touched.
|
|
117
|
+
* See
|
|
118
|
+
* [ADR-032](https://github.com/GMOD/jbrowse-components/blob/main/agent-docs/architecture-decision-records/adr-032-track-config-nodes-are-throwaway-views.md).
|
|
119
|
+
*
|
|
120
|
+
* Not evicted: it's a pure memoization cache, bounded by the count of
|
|
121
|
+
* distinct tracks shown this session (each entry a lazily-hydrated config
|
|
122
|
+
* node), holding no authoritative state — the persisted delta is the
|
|
123
|
+
* source of truth, and reset/programmatic edits keep a retained copy in
|
|
124
|
+
* sync. Retention is volatile RAM only (never serialized), so it's not
|
|
125
|
+
* worth a reference-counted prune at every track-removal path.
|
|
126
|
+
*
|
|
127
|
+
* Each entry carries the resolved config it mirrors, so a delta or base
|
|
128
|
+
* replaced from outside this mixin invalidates it — see
|
|
129
|
+
* `getEditableTrackConfig`.
|
|
130
|
+
*/
|
|
131
|
+
editableTrackConfigs: new Map(),
|
|
132
|
+
}))
|
|
133
|
+
.views(self => {
|
|
134
|
+
// Memoize merged configs per (base object, delta value) pair so the tracks
|
|
135
|
+
// getter returns stable object identity across unrelated recomputes. A
|
|
136
|
+
// fresh merged object each time would rehydrate a new MST node in
|
|
137
|
+
// TrackConfigurationReference, losing open display state (see CLAUDE.md).
|
|
138
|
+
// Both keys have stable identity until they actually change: a track's
|
|
139
|
+
// delta only when that track is edited, and the base only on a
|
|
140
|
+
// jbrowse.tracks write or a sessionTracks entry replaced. This relies on
|
|
141
|
+
// a base config never mutating in place. If an in-place base edit is ever
|
|
142
|
+
// added, key this cache on base content too.
|
|
143
|
+
const mergeCache = new WeakMap();
|
|
144
|
+
function withDelta(base, delta) {
|
|
145
|
+
if (!delta) {
|
|
146
|
+
return base;
|
|
147
|
+
}
|
|
148
|
+
const cached = mergeCache.get(base);
|
|
149
|
+
if (cached?.delta === delta) {
|
|
150
|
+
return cached.merged;
|
|
151
|
+
}
|
|
152
|
+
const merged = mergeTrackConfig(toPlainConfig(base), delta);
|
|
153
|
+
mergeCache.set(base, { delta, merged });
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
156
|
+
// The bases in `tracks` order, each id's base and each id's position.
|
|
157
|
+
// A config.json that repeats an id resolves to its last entry.
|
|
158
|
+
const trackBaseIndex = computed(() => {
|
|
159
|
+
const entries = sessionEntries(self);
|
|
160
|
+
const sessionIds = new Set(entries.map(t => t.trackId));
|
|
161
|
+
const list = [
|
|
162
|
+
...entries,
|
|
163
|
+
...baseTracks(self).filter(t => !sessionIds.has(t.trackId)),
|
|
164
|
+
];
|
|
165
|
+
const byId = new Map();
|
|
166
|
+
const at = new Map();
|
|
167
|
+
for (let i = 0; i < list.length; i++) {
|
|
168
|
+
const { trackId } = list[i];
|
|
169
|
+
byId.set(trackId, list[i]);
|
|
170
|
+
at.set(trackId, i);
|
|
171
|
+
}
|
|
172
|
+
return { list, byId, at };
|
|
173
|
+
}, { name: 'trackBaseIndex' });
|
|
174
|
+
// A write replaces the whole delta map but keeps every other id's delta
|
|
175
|
+
// by identity, so reading one id's through its own computed wakes only
|
|
176
|
+
// the edited track's readers.
|
|
177
|
+
const deltaFor = perIdComputed(trackId => self.trackConfigDeltas[trackId]);
|
|
178
|
+
const baseFor = perIdComputed(trackId => {
|
|
179
|
+
const base = self.trackBasesById.get(trackId);
|
|
180
|
+
return base
|
|
181
|
+
? toPlainConfig(base)
|
|
182
|
+
: undefined;
|
|
183
|
+
});
|
|
184
|
+
const editableBySchema = new Map();
|
|
185
|
+
return {
|
|
186
|
+
/**
|
|
187
|
+
* #getter
|
|
188
|
+
* Each track's base by trackId: its sessionTracks entry, else its
|
|
189
|
+
* config.json entry. Rebuilt when either list changes, never on an
|
|
190
|
+
* edit.
|
|
191
|
+
*/
|
|
192
|
+
get trackBasesById() {
|
|
193
|
+
return trackBaseIndex.get().byId;
|
|
194
|
+
},
|
|
195
|
+
/**
|
|
196
|
+
* #method
|
|
197
|
+
* `base` with its delta (trackConfigDeltas) merged over it, or `base`
|
|
198
|
+
* itself by identity when it has none, which keeps the hydration cache
|
|
199
|
+
* warm.
|
|
200
|
+
*/
|
|
201
|
+
withTrackEdits(base) {
|
|
202
|
+
const plain = base;
|
|
203
|
+
return withDelta(plain, deltaFor(plain.trackId));
|
|
204
|
+
},
|
|
205
|
+
/**
|
|
206
|
+
* #getter
|
|
207
|
+
* Session tracks first, then the config tracks, each with its edits.
|
|
208
|
+
* An edit copies the list of bases and lays each delta at its track's
|
|
209
|
+
* position, so past that copy it costs the count of edited tracks.
|
|
210
|
+
*/
|
|
211
|
+
get tracks() {
|
|
212
|
+
const { list, at } = trackBaseIndex.get();
|
|
213
|
+
const tracks = list.slice();
|
|
214
|
+
for (const [trackId, delta] of Object.entries(self.trackConfigDeltas)) {
|
|
215
|
+
const i = at.get(trackId);
|
|
216
|
+
if (i !== undefined) {
|
|
217
|
+
tracks[i] = withDelta(list[i], delta);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return tracks;
|
|
221
|
+
},
|
|
222
|
+
/**
|
|
223
|
+
* #method
|
|
224
|
+
* The entry `trackId` edits over, hydrated: the one the session added
|
|
225
|
+
* it with, or its config.json entry. What a display's "is this
|
|
226
|
+
* arranged" and "reset" compare its live config against. Per-id
|
|
227
|
+
* reactive, like `getTrackById`.
|
|
228
|
+
*/
|
|
229
|
+
baseTrackConfig(trackId) {
|
|
230
|
+
return baseFor(trackId);
|
|
231
|
+
},
|
|
232
|
+
/**
|
|
233
|
+
* #method
|
|
234
|
+
* The config node `trackId` resolves to: a live node as it stands,
|
|
235
|
+
* otherwise a private working copy of its current frozen (base+delta)
|
|
236
|
+
* config, so a shown track's in-place quick-edits (setSlot) mutate
|
|
237
|
+
* this copy and never the shared frozen base (see ADR-032). Called by
|
|
238
|
+
* TrackConfigurationReference during lazy hydration.
|
|
239
|
+
*
|
|
240
|
+
* Cached against the resolved config it was built from, not by trackId
|
|
241
|
+
* alone. A write this mixin makes re-stamps the entry, so the copy an
|
|
242
|
+
* edit is still being typed into is never swapped out mid-keystroke;
|
|
243
|
+
* a delta or base replaced from outside — an undo's `applySnapshot` on
|
|
244
|
+
* the session, a session restore, a session track deleted and added
|
|
245
|
+
* again under its id — cannot, so the next read rebuilds the copy from
|
|
246
|
+
* what now resolves. Per-id reactive on the node rather than the
|
|
247
|
+
* config, so a display reading its config does not recompute when
|
|
248
|
+
* its own working copy is persisted. Per schema type as well, since
|
|
249
|
+
* an id re-added under another track type resolves through that
|
|
250
|
+
* type's reference.
|
|
251
|
+
*/
|
|
252
|
+
getEditableTrackConfig(trackId, schemaType) {
|
|
253
|
+
let editable = editableBySchema.get(schemaType);
|
|
254
|
+
if (!editable) {
|
|
255
|
+
editable = perIdComputed(id => {
|
|
256
|
+
const resolved = self.getTrackById(id);
|
|
257
|
+
if (!resolved || isStateTreeNode(resolved)) {
|
|
258
|
+
return resolved;
|
|
259
|
+
}
|
|
260
|
+
const existing = self.editableTrackConfigs.get(id);
|
|
261
|
+
if (existing?.source === resolved) {
|
|
262
|
+
return existing.node;
|
|
263
|
+
}
|
|
264
|
+
const node = schemaType.create(resolved, { pluginManager });
|
|
265
|
+
self.editableTrackConfigs.set(id, { node, source: resolved });
|
|
266
|
+
return node;
|
|
267
|
+
});
|
|
268
|
+
editableBySchema.set(schemaType, editable);
|
|
269
|
+
}
|
|
270
|
+
return editable(trackId);
|
|
271
|
+
},
|
|
272
|
+
/**
|
|
273
|
+
* #method
|
|
274
|
+
* The overridden slots for `trackId` (empty when it has no delta): each
|
|
275
|
+
* changed setting's path, its base/default value and the edited value.
|
|
276
|
+
* Drives the "view changes" dialog opened from the edited badge.
|
|
277
|
+
*/
|
|
278
|
+
getTrackConfigChanges(trackId) {
|
|
279
|
+
// Every rendered track row asks this (the edited badge); only an
|
|
280
|
+
// edited one has a delta, so an unedited row subscribes to no base.
|
|
281
|
+
const delta = deltaFor(trackId);
|
|
282
|
+
const base = delta ? baseFor(trackId) : undefined;
|
|
283
|
+
return delta && base ? flattenTrackConfigDelta(base, delta) : [];
|
|
284
|
+
},
|
|
285
|
+
};
|
|
9
286
|
})
|
|
10
287
|
.views(self => ({
|
|
11
|
-
|
|
12
|
-
|
|
288
|
+
/**
|
|
289
|
+
* #method
|
|
290
|
+
* Whether `trackId` carries an edit over its base config, which drives
|
|
291
|
+
* the "Reset track settings" item and the edited badge. Changed slots,
|
|
292
|
+
* not mere presence in trackConfigDeltas: a delta can hold only
|
|
293
|
+
* content-free display stubs, which must not read as an override.
|
|
294
|
+
*/
|
|
295
|
+
isTrackOverride(trackId) {
|
|
296
|
+
return self.getTrackConfigChanges(trackId).length > 0;
|
|
297
|
+
},
|
|
298
|
+
/**
|
|
299
|
+
* #getter
|
|
300
|
+
* The edited tracks `promoteTrackConfigDeltas` can write: those whose
|
|
301
|
+
* base is a config.json entry. A session track's delta has no file to
|
|
302
|
+
* go to.
|
|
303
|
+
*/
|
|
304
|
+
get promotableTrackIds() {
|
|
305
|
+
const sessionIds = new Set(self.sessionTracks.map(t => t.trackId));
|
|
306
|
+
return Object.keys(self.trackConfigDeltas).filter(id => !sessionIds.has(id) && self.trackBasesById.has(id));
|
|
307
|
+
},
|
|
308
|
+
}))
|
|
309
|
+
.actions(self => ({
|
|
310
|
+
afterAttach() {
|
|
311
|
+
// One-time format upgrade: a legacy session stored a non-admin's edits as
|
|
312
|
+
// a full-config sessionTracks entry shadowing the same-id admin track.
|
|
313
|
+
// Convert those to deltas so the whole app uses one override mechanism.
|
|
314
|
+
// Genuinely-added session tracks (no matching base) are left in place.
|
|
315
|
+
const configById = new Map(baseTracks(self).map(t => [t.trackId, t]));
|
|
316
|
+
const legacy = self.sessionTracks.filter(t => configById.has(t.trackId));
|
|
317
|
+
if (legacy.length > 0) {
|
|
318
|
+
const deltas = { ...self.trackConfigDeltas };
|
|
319
|
+
for (const track of legacy) {
|
|
320
|
+
const plainBase = toPlainConfig(configById.get(track.trackId));
|
|
321
|
+
const delta = diffTrackConfig(plainBase, getSnapshot(track));
|
|
322
|
+
// a legacy override identical to its base contributes no changed
|
|
323
|
+
// slots: drop it rather than migrate a content-free delta
|
|
324
|
+
if (deltaHasChanges(plainBase, delta)) {
|
|
325
|
+
deltas[track.trackId] = delta;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
self.trackConfigDeltas = deltas;
|
|
329
|
+
for (const track of legacy) {
|
|
330
|
+
self.sessionTracks.remove(track);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
13
333
|
},
|
|
14
334
|
}))
|
|
15
335
|
.actions(self => {
|
|
16
|
-
const {
|
|
336
|
+
const { publishTrackConf: superPublishTrackConf, deleteTrackConf: superDeleteTrackConf, updateTrackConfiguration: superUpdateTrackConfiguration, } = self;
|
|
337
|
+
// A cleared delta reverts the track's live working copy to the base in
|
|
338
|
+
// place, so an open view re-renders to the default. applySnapshot keeps
|
|
339
|
+
// the node identity (existing observers just update); no-op for a track
|
|
340
|
+
// that was never shown (no working copy).
|
|
341
|
+
function revertEditableTrackConfig(trackId) {
|
|
342
|
+
const entry = self.editableTrackConfigs.get(trackId);
|
|
343
|
+
const base = self.baseTrackConfig(trackId);
|
|
344
|
+
if (entry && base) {
|
|
345
|
+
applySnapshot(entry.node, base);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Re-stamp a working copy with what its track resolves to after a write,
|
|
349
|
+
// so the copy this mixin just persisted from stays the one the next read
|
|
350
|
+
// resolves.
|
|
351
|
+
function stampEditableTrackConfig(trackId) {
|
|
352
|
+
const entry = self.editableTrackConfigs.get(trackId);
|
|
353
|
+
if (entry) {
|
|
354
|
+
entry.source = self.getTrackById(trackId);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// Single writer for trackConfigDeltas (pass undefined to clear). Clearing
|
|
358
|
+
// also reverts the working copy, which is what makes a Reset visible in an
|
|
359
|
+
// open view.
|
|
360
|
+
function writeDelta(trackId, delta) {
|
|
361
|
+
self.trackConfigDeltas = delta
|
|
362
|
+
? { ...self.trackConfigDeltas, [trackId]: delta }
|
|
363
|
+
: withoutDelta(self.trackConfigDeltas, trackId);
|
|
364
|
+
if (!delta) {
|
|
365
|
+
revertEditableTrackConfig(trackId);
|
|
366
|
+
}
|
|
367
|
+
stampEditableTrackConfig(trackId);
|
|
368
|
+
}
|
|
369
|
+
// Push a *programmatic* update (the config editor's Apply, or any
|
|
370
|
+
// updateTrackConfiguration not driven by this node's own live edits) into
|
|
371
|
+
// the working copy. When the node IS the edit source its snapshot already
|
|
372
|
+
// equals fullConfig, so this skips — which also avoids clobbering an
|
|
373
|
+
// in-progress live drag.
|
|
374
|
+
function syncEditableTrackConfig(trackId, fullConfig) {
|
|
375
|
+
const entry = self.editableTrackConfigs.get(trackId);
|
|
376
|
+
if (entry && !compareStructural(getSnapshot(entry.node), fullConfig)) {
|
|
377
|
+
applySnapshot(entry.node, fullConfig);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// A working copy's own snapshot, which both savers pass, is already in
|
|
381
|
+
// the schema's form; anything else a caller hands in may not be.
|
|
382
|
+
function inTrackForm(trackConf) {
|
|
383
|
+
const entry = self.editableTrackConfigs.get(trackConf.trackId);
|
|
384
|
+
return entry && getSnapshot(entry.node) === trackConf
|
|
385
|
+
? trackConf
|
|
386
|
+
: hydrate(trackConf);
|
|
387
|
+
}
|
|
388
|
+
// The session-scoped add, shared by the action that always means the
|
|
389
|
+
// session and the one that means it only for a non-admin. A plain closure
|
|
390
|
+
// rather than `this.addSessionTrackConf` so neither action's inferred
|
|
391
|
+
// return type depends on the other's.
|
|
392
|
+
function addToSession(loose) {
|
|
393
|
+
const trackConf = expandLooseTrackConfig(loose, pluginManager);
|
|
394
|
+
const { trackId, type } = trackConf;
|
|
395
|
+
if (!type) {
|
|
396
|
+
throw new Error(`track type not specified for "${trackId}"`);
|
|
397
|
+
}
|
|
398
|
+
assertTrackConfOutlivesItsAssemblies(self, trackConf, 'sessionTracks');
|
|
399
|
+
// Dedupe against everything the session can already resolve — config
|
|
400
|
+
// catalog (jbrowse.tracks), assembly sequences, connection tracks and
|
|
401
|
+
// prior sessionTracks — not just sessionTracks. Re-adding a config
|
|
402
|
+
// already in the catalog would otherwise push a full shadow into
|
|
403
|
+
// sessionTracks, silently demoting a catalog track to a session track
|
|
404
|
+
// and dropping its trackConfigDeltas override semantics.
|
|
405
|
+
const existing = self.getTrackById(trackId);
|
|
406
|
+
if (existing) {
|
|
407
|
+
const entry = self.sessionTracks.find(t => t.trackId === trackId);
|
|
408
|
+
if (entry) {
|
|
409
|
+
assertNotReaddedDifferently(pluginManager, entry, {
|
|
410
|
+
...trackConf,
|
|
411
|
+
trackId,
|
|
412
|
+
type,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return existing;
|
|
416
|
+
}
|
|
417
|
+
// sessionTracks is a typed MST array (unlike the frozen
|
|
418
|
+
// jbrowse.tracks), so an invalid config throws on push. Surface it as
|
|
419
|
+
// a snackbar and skip the add, rather than letting it crash the app.
|
|
420
|
+
try {
|
|
421
|
+
const length = self.sessionTracks.push(trackConf);
|
|
422
|
+
return self.sessionTracks[length - 1];
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
self.notifyError(`Track "${trackId}" has an invalid configuration: ${e}`, e);
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
17
429
|
return {
|
|
430
|
+
/**
|
|
431
|
+
* #action
|
|
432
|
+
* Add a track config to *this session*: it lands in `sessionTracks`,
|
|
433
|
+
* travels with the session when it is saved or shared, and never
|
|
434
|
+
* reaches the config.json the server hands every visitor.
|
|
435
|
+
*
|
|
436
|
+
* **The default destination.** Everything that is not an Add-track
|
|
437
|
+
* workflow wants this one, whoever is looking — a session spec's
|
|
438
|
+
* `sessionTracks`, a URL's `&sessionTracks=`, and every track a feature
|
|
439
|
+
* stands up on the user's behalf. Mirrors `addSessionConnectionConf` in
|
|
440
|
+
* the connections mixins.
|
|
441
|
+
*
|
|
442
|
+
* Returns the entry, which is the track's base: edit the track through
|
|
443
|
+
* `updateTrackConfiguration` or a display's `setConf`, not the entry.
|
|
444
|
+
*/
|
|
445
|
+
addSessionTrackConf(trackConf) {
|
|
446
|
+
return addToSession(trackConf);
|
|
447
|
+
},
|
|
448
|
+
/**
|
|
449
|
+
* #action
|
|
450
|
+
* Publish a track config to the shared catalog if this user can, and
|
|
451
|
+
* fall back to their session if they cannot: an admin's goes to
|
|
452
|
+
* `jbrowse.tracks`, which the admin server writes back into the
|
|
453
|
+
* config.json every visitor is served; everyone else's goes to
|
|
454
|
+
* `sessionTracks`.
|
|
455
|
+
*
|
|
456
|
+
* **Call it only from the "Add track" workflows**, where an admin
|
|
457
|
+
* adding a track means to add it for the whole site. For a track a
|
|
458
|
+
* feature creates on the user's behalf (a search result, a computed
|
|
459
|
+
* consensus, a reconstruction's segment labels), one admin click on
|
|
460
|
+
* this publishes it to every visitor, and each later click publishes
|
|
461
|
+
* another copy, because the per-launch trackId prevents deduplication.
|
|
462
|
+
* Use `addSessionTrackConf` above for those.
|
|
463
|
+
*
|
|
464
|
+
* An admin's track still goes to the session when it names an assembly
|
|
465
|
+
* the config.json does not carry: a session spec's assembly, a MAF
|
|
466
|
+
* sample's genome, or a comparative view's synthesized pair. The
|
|
467
|
+
* Add-track widget takes its assembly from the containing view, so an
|
|
468
|
+
* admin adding a track in such a view passes a name no visitor can
|
|
469
|
+
* resolve. Publishing it would write an entry the server sends to every
|
|
470
|
+
* visitor and no visitor can draw. Adding it to the session keeps the
|
|
471
|
+
* track usable and writes no dangling entry; a snackbar names the
|
|
472
|
+
* assembly that caused the fallback.
|
|
473
|
+
*
|
|
474
|
+
* **The snackbar names the assembly and not the track**, so a batch
|
|
475
|
+
* produces one message: `pushSnackbarMessage` dedupes on the exact
|
|
476
|
+
* text, and `BulkAddTracksWorkflow` publishes in a loop. Including the
|
|
477
|
+
* track name would show one snackbar per file to an admin adding
|
|
478
|
+
* thirty. The assembly is all the admin needs to act on.
|
|
479
|
+
*/
|
|
480
|
+
publishTrackConf(trackConf) {
|
|
481
|
+
if (!self.adminMode) {
|
|
482
|
+
return addToSession(trackConf);
|
|
483
|
+
}
|
|
484
|
+
const missing = assembliesNotInTheCatalog(self, trackConf);
|
|
485
|
+
if (!missing.length) {
|
|
486
|
+
return superPublishTrackConf(trackConf);
|
|
487
|
+
}
|
|
488
|
+
self.notify(`A track naming assembly "${missing.join('", "')}" goes to this session rather than to the site configuration: the config.json does not carry that assembly, so a published track naming it would be served to every visitor and drawn by none of them.`, 'info');
|
|
489
|
+
return addToSession(trackConf);
|
|
490
|
+
},
|
|
491
|
+
/**
|
|
492
|
+
* #action
|
|
493
|
+
* Deprecated alias of `addSessionTrackConf`. Call that, or
|
|
494
|
+
* `publishTrackConf`.
|
|
495
|
+
*
|
|
496
|
+
* @deprecated
|
|
497
|
+
*
|
|
498
|
+
* The session-scoped add under its old name, for the prebuilt plugin
|
|
499
|
+
* bundles that reach it by name at runtime. See the base mixin's copy
|
|
500
|
+
* for why it survives and why it now means the session; nothing in tree
|
|
501
|
+
* may call it.
|
|
502
|
+
*/
|
|
18
503
|
addTrackConf(trackConf) {
|
|
19
|
-
|
|
20
|
-
|
|
504
|
+
return addToSession(trackConf);
|
|
505
|
+
},
|
|
506
|
+
/**
|
|
507
|
+
* #action
|
|
508
|
+
* Persist an edited track config as a delta (trackConfigDeltas) against
|
|
509
|
+
* its base, the sessionTracks or config.json entry — only the changed
|
|
510
|
+
* slots — so the edits persist, are shared, undo and reset, while
|
|
511
|
+
* changes to untouched fields of the base still flow through. An
|
|
512
|
+
* admin's edit is a delta like anyone's: it reaches the config.json the
|
|
513
|
+
* server hands every visitor only through `promoteTrackConfigDeltas`.
|
|
514
|
+
* An opened connection track defers to the base mixin, which routes it
|
|
515
|
+
* to connectionTrackConfigs.
|
|
516
|
+
*/
|
|
517
|
+
updateTrackConfiguration(trackConf) {
|
|
518
|
+
const { trackId } = trackConf;
|
|
519
|
+
const plainBase = self.baseTrackConfig(trackId);
|
|
520
|
+
if (plainBase) {
|
|
521
|
+
const delta = diffTrackConfig(plainBase, inTrackForm(trackConf));
|
|
522
|
+
// an edit that nets back to the base clears any prior override
|
|
523
|
+
if (deltaHasChanges(plainBase, delta)) {
|
|
524
|
+
// Two views showing the same track each run BaseTrackModel's
|
|
525
|
+
// persist reaction against the shared config node, so a single edit
|
|
526
|
+
// calls this twice with an identical delta (and the config editor
|
|
527
|
+
// can re-save an unchanged config). Skip a structurally-identical
|
|
528
|
+
// re-store: writing a fresh trackConfigDeltas object would churn its
|
|
529
|
+
// identity and make the tracks getter rehydrate a new merged node
|
|
530
|
+
// for no real change.
|
|
531
|
+
const existing = self.trackConfigDeltas[trackId];
|
|
532
|
+
if (!existing || !compareStructural(existing, delta)) {
|
|
533
|
+
writeDelta(trackId, delta);
|
|
534
|
+
}
|
|
535
|
+
syncEditableTrackConfig(trackId, trackConf);
|
|
536
|
+
}
|
|
537
|
+
else if (trackId in self.trackConfigDeltas) {
|
|
538
|
+
writeDelta(trackId, undefined);
|
|
539
|
+
}
|
|
21
540
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
541
|
+
else {
|
|
542
|
+
// an opened connection track, or a homeless in-memory-only edit
|
|
543
|
+
superUpdateTrackConfiguration(trackConf);
|
|
544
|
+
syncEditableTrackConfig(trackId, trackConf);
|
|
545
|
+
stampEditableTrackConfig(trackId);
|
|
25
546
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
547
|
+
},
|
|
548
|
+
/**
|
|
549
|
+
* #action
|
|
550
|
+
* Write this session's track edits into the base configs — the
|
|
551
|
+
* config.json an admin server hands every visitor — and drop the
|
|
552
|
+
* deltas they were held in. One track's, or every track's when
|
|
553
|
+
* `trackId` is omitted, of `promotableTrackIds`: a session track's
|
|
554
|
+
* edits stay in the session. An admin's alone: anyone else's base is
|
|
555
|
+
* the in-memory copy of a file they cannot write.
|
|
556
|
+
*/
|
|
557
|
+
promoteTrackConfigDeltas(trackId) {
|
|
558
|
+
if (!self.adminMode) {
|
|
559
|
+
throw new Error('only an admin can save track settings to config');
|
|
560
|
+
}
|
|
561
|
+
const promotable = self.promotableTrackIds;
|
|
562
|
+
const ids = trackId === undefined
|
|
563
|
+
? promotable
|
|
564
|
+
: promotable.includes(trackId)
|
|
565
|
+
? [trackId]
|
|
566
|
+
: [];
|
|
567
|
+
const bases = self.trackBasesById;
|
|
568
|
+
for (const id of ids) {
|
|
569
|
+
const base = bases.get(id);
|
|
570
|
+
self.jbrowse.updateTrackConf(mergeTrackConfig(toPlainConfig(base), self.trackConfigDeltas[id]));
|
|
571
|
+
writeDelta(id, undefined);
|
|
29
572
|
}
|
|
30
|
-
const length = self.sessionTracks.push(trackConf);
|
|
31
|
-
return self.sessionTracks[length - 1];
|
|
32
573
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
574
|
+
/**
|
|
575
|
+
* #action
|
|
576
|
+
* Drop a track's delta (trackConfigDeltas) so it reverts to its base,
|
|
577
|
+
* the sessionTracks or jbrowse.tracks entry. Unlike deleteTrackConf this
|
|
578
|
+
* does not dereference the track from open views — the base config
|
|
579
|
+
* re-resolves in place, so an open track stays open and simply reverts.
|
|
580
|
+
*/
|
|
581
|
+
resetTrackConfiguration(trackId) {
|
|
582
|
+
if (trackId in self.trackConfigDeltas) {
|
|
583
|
+
writeDelta(trackId, undefined);
|
|
37
584
|
}
|
|
585
|
+
},
|
|
586
|
+
/**
|
|
587
|
+
* #action
|
|
588
|
+
*/
|
|
589
|
+
deleteTrackConf(trackConf) {
|
|
590
|
+
superDeleteTrackConf(trackConf);
|
|
38
591
|
const { trackId } = trackConf;
|
|
592
|
+
// A delta only outlives its base if the base is gone, so drop it here
|
|
593
|
+
// rather than strand it.
|
|
594
|
+
if (trackId in self.trackConfigDeltas) {
|
|
595
|
+
writeDelta(trackId, undefined);
|
|
596
|
+
}
|
|
39
597
|
const idx = self.sessionTracks.findIndex(t => t.trackId === trackId);
|
|
40
598
|
if (idx === -1) {
|
|
41
599
|
return undefined;
|
|
@@ -43,18 +601,10 @@ export function SessionTracksManagerSessionMixin(pluginManager) {
|
|
|
43
601
|
return self.sessionTracks.splice(idx, 1);
|
|
44
602
|
},
|
|
45
603
|
};
|
|
46
|
-
})
|
|
47
|
-
.postProcessSnapshot(snap => {
|
|
48
|
-
if (!snap) {
|
|
49
|
-
return snap;
|
|
50
|
-
}
|
|
51
|
-
const { sessionTracks, ...rest } = snap;
|
|
52
|
-
return {
|
|
53
|
-
...rest,
|
|
54
|
-
...(sessionTracks?.length ? { sessionTracks } : {}),
|
|
55
|
-
};
|
|
56
604
|
});
|
|
57
605
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
606
|
+
// The `isSessionWithSessionTracks` guard that used to live here now lives in
|
|
607
|
+
// `@jbrowse/core/util/types` beside the rest of the session capability guards.
|
|
608
|
+
// Its only caller was the hierarchical track selector — a plugin, which could
|
|
609
|
+
// not reach product-core through the ABI it publishes against, so this package
|
|
610
|
+
// was a runtime dependency of two shipped plugins for three type guards.
|