@jbrowse/product-core 4.3.0 → 5.0.0-beta.2
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 +629 -304
- package/esm/RootModel/BaseRootModel.js +139 -14
- package/esm/RootModel/HierarchicalConfig.d.ts +57 -21
- 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 +68 -0
- package/esm/RootModel/PreferencesConfig.js +76 -0
- package/esm/RootModel/createConfigModel.d.ts +240 -0
- package/esm/RootModel/createConfigModel.js +56 -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 +143 -0
- package/esm/Session/BaseSession.d.ts +312 -11
- package/esm/Session/BaseSession.js +458 -14
- package/esm/Session/Connections.d.ts +114 -8
- package/esm/Session/Connections.js +195 -74
- package/esm/Session/DrawerWidgets.d.ts +115 -12
- package/esm/Session/DrawerWidgets.js +161 -42
- package/esm/Session/MultipleViews.d.ts +1281 -1128
- package/esm/Session/MultipleViews.js +396 -64
- package/esm/Session/Preferences.d.ts +1667 -0
- package/esm/Session/Preferences.js +59 -0
- package/esm/Session/ReferenceManagement.d.ts +28 -3
- package/esm/Session/ReferenceManagement.js +56 -41
- package/esm/Session/SessionTracks.d.ts +1277 -1138
- package/esm/Session/SessionTracks.js +540 -35
- package/esm/Session/Themes.d.ts +87 -2
- package/esm/Session/Themes.js +156 -23
- package/esm/Session/TrackMenu.d.ts +112 -0
- package/esm/Session/TrackMenu.js +200 -0
- package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
- package/esm/Session/TrackMenuSessionMixin.js +41 -0
- package/esm/Session/Tracks.d.ts +1171 -1117
- package/esm/Session/Tracks.js +183 -20
- package/esm/Session/assertSessionModel.d.ts +21 -0
- package/esm/Session/assertSessionModel.js +1 -0
- package/esm/Session/developerMode.d.ts +7 -0
- package/esm/Session/developerMode.js +43 -0
- package/esm/Session/finalizeSession.d.ts +16 -0
- package/esm/Session/finalizeSession.js +29 -0
- package/esm/Session/index.d.ts +22 -10
- package/esm/Session/index.js +13 -10
- package/esm/Session/readdedTrackConf.d.ts +8 -0
- package/esm/Session/readdedTrackConf.js +25 -0
- package/esm/Session/shareableSnapshot.d.ts +77 -0
- package/esm/Session/shareableSnapshot.js +196 -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 +56 -0
- package/esm/controllerTracks.js +90 -0
- package/esm/destroyViewState.d.ts +21 -0
- package/esm/destroyViewState.js +29 -0
- package/esm/index.d.ts +31 -4
- package/esm/index.js +31 -4
- package/esm/loadPlugins.d.ts +33 -0
- package/esm/loadPlugins.js +41 -0
- package/esm/localFiles.d.ts +74 -0
- package/esm/localFiles.js +156 -0
- package/esm/observeSession.d.ts +56 -0
- package/esm/observeSession.js +91 -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 +568 -0
- package/esm/resolveAssemblies.d.ts +44 -0
- package/esm/resolveAssemblies.js +55 -0
- package/esm/rpcWorker.d.ts +26 -0
- package/esm/rpcWorker.js +98 -16
- package/esm/scheduleDetachedDestroy.d.ts +18 -0
- package/esm/scheduleDetachedDestroy.js +24 -0
- package/esm/sessionMigrations/index.d.ts +53 -0
- package/esm/sessionMigrations/index.js +432 -0
- package/esm/sessionUrl.d.ts +41 -0
- package/esm/sessionUrl.js +53 -0
- package/esm/sessionUtils.d.ts +69 -0
- package/esm/sessionUtils.js +613 -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 +3 -6
- package/esm/ui/AboutDialog.js +4 -4
- package/esm/ui/AboutDialogContents.d.ts +2 -6
- package/esm/ui/AboutDialogContents.js +34 -13
- package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
- package/esm/ui/AssemblyInfoPanel.js +69 -0
- package/esm/ui/DisplayDefaultsSection.d.ts +36 -0
- package/esm/ui/DisplayDefaultsSection.js +65 -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 +35 -26
- package/esm/ui/HeaderButtons.d.ts +7 -1
- package/esm/ui/HeaderButtons.js +23 -18
- package/esm/ui/ModalWidget.d.ts +12 -0
- package/esm/ui/ModalWidget.js +33 -0
- package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
- package/esm/ui/ModalWidgetAppBar.js +9 -0
- package/esm/ui/PreferencesDialog.d.ts +56 -0
- package/esm/ui/PreferencesDialog.js +145 -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 +57 -63
- 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/drawerLayout.d.ts +18 -0
- package/esm/ui/drawerLayout.js +22 -0
- package/esm/ui/index.d.ts +13 -1
- package/esm/ui/index.js +12 -1
- package/esm/ui/util.d.ts +37 -4
- package/esm/ui/util.js +35 -38
- package/esm/useEngineLifecycle.d.ts +19 -0
- package/esm/useEngineLifecycle.js +44 -0
- package/package.json +12 -15
- 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,554 @@
|
|
|
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 } from 'mobx';
|
|
6
|
+
import { TracksManagerSessionMixin } from './Tracks.js';
|
|
7
|
+
import { assertNotReaddedDifferently } from './readdedTrackConf.js';
|
|
8
|
+
import { assertTrackConfOutlivesItsAssemblies } from './temporaryAssemblyTracks.js';
|
|
9
|
+
// jbrowse.tracks holds frozen plain objects (app-core, web/desktop) or MST
|
|
10
|
+
// config nodes (product-core, embedded react views); the delta math reads both
|
|
11
|
+
// as plain track configs. Single site for that documented cast — per-node
|
|
12
|
+
// normalization still happens in toPlainConfig.
|
|
13
|
+
function baseTracks(self) {
|
|
14
|
+
return self.jbrowse.tracks;
|
|
15
|
+
}
|
|
16
|
+
// A delta must not be stored unless it records a real user edit, or it would
|
|
17
|
+
// flip isTrackOverride (edited badge + Reset menu) on with nothing actually
|
|
18
|
+
// overridden. Two ways an empty edit slips past a key count: diffTrackConfig
|
|
19
|
+
// always retains the self-identifying trackId, and a base config that omits
|
|
20
|
+
// `displays` diffs against the hydrated snapshot's injected {type, displayId}
|
|
21
|
+
// display stubs to yield `{trackId, displays: [...stubs]}` — nonzero keys but no
|
|
22
|
+
// changed slot. flattenTrackConfigDelta (which drops identity keys and empty
|
|
23
|
+
// display stubs) is the honest test of whether any real setting changed.
|
|
24
|
+
function deltaHasChanges(base, delta) {
|
|
25
|
+
return flattenTrackConfigDelta(base, delta).length > 0;
|
|
26
|
+
}
|
|
27
|
+
// The assemblies a track config names that the config.json does not carry.
|
|
28
|
+
// `self.jbrowse.assemblies` is the catalog alone — the session's own
|
|
29
|
+
// `assemblies` getter adds sessionAssemblies to it, which is the opposite of
|
|
30
|
+
// what this asks.
|
|
31
|
+
//
|
|
32
|
+
// An assembly's `aliases` are names of that assembly, so a track naming one
|
|
33
|
+
// names something the catalog carries and publishes like any other. Matching on
|
|
34
|
+
// `name` alone diverted it to the session and told the admin the config.json
|
|
35
|
+
// has no such assembly while they were looking at it. Read off the configs
|
|
36
|
+
// rather than through the assembly manager: the question is what the config.json
|
|
37
|
+
// declares, and the manager also answers for sessionAssemblies, which is the
|
|
38
|
+
// case this exists to catch.
|
|
39
|
+
function assembliesNotInTheCatalog(self, trackConf) {
|
|
40
|
+
const catalog = new Set(self.jbrowse.assemblies.flatMap(a => [
|
|
41
|
+
readConfObject(a, 'name'),
|
|
42
|
+
...(readConfObject(a, 'aliases') ?? []),
|
|
43
|
+
]));
|
|
44
|
+
const names = readConfObject(trackConf, 'assemblyNames');
|
|
45
|
+
return names?.filter(name => !catalog.has(name)) ?? [];
|
|
46
|
+
}
|
|
47
|
+
function withoutDelta(deltas, trackId) {
|
|
48
|
+
const { [trackId]: _dropped, ...rest } = deltas;
|
|
49
|
+
return rest;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* #stateModel SessionTracksManagerSessionMixin
|
|
53
|
+
*/
|
|
4
54
|
export function SessionTracksManagerSessionMixin(pluginManager) {
|
|
55
|
+
// A jbrowse.tracks base entry and a shown track's persisted snapshot are in
|
|
56
|
+
// two different config "normal forms". A base (app-core web/desktop) is the
|
|
57
|
+
// raw config-file object: shorthand `uri`, no injected display stubs. But a
|
|
58
|
+
// shown track's snapshot comes from a live hydrated config node, so it is
|
|
59
|
+
// post-preProcessSnapshot: the `uri` shorthand is expanded to
|
|
60
|
+
// bamLocation/index, `baseUri` is propagated into those locations, and a
|
|
61
|
+
// {type, displayId} stub is injected per compatible display type. Diffing or
|
|
62
|
+
// merging across those two forms makes every expanded/injected field read as
|
|
63
|
+
// a user edit — pinning whole adapters and content-free display stubs into
|
|
64
|
+
// deltas, and defeating the delta's purpose (a later admin adapter-URL fix
|
|
65
|
+
// would then be masked by the pinned copy). Normalize a base to the hydrated
|
|
66
|
+
// form by running it through the same track schema, so diff/merge compare
|
|
67
|
+
// like with like and cancel everything untouched. Memoized per frozen-base
|
|
68
|
+
// identity (stable until a jbrowse.tracks write). A base that is already an
|
|
69
|
+
// MST config node (product-core embedded views) is snapshotted directly — it
|
|
70
|
+
// is already in the hydrated form.
|
|
71
|
+
//
|
|
72
|
+
// The isStateTreeNode/getSnapshot casts are load-bearing, don't "simplify"
|
|
73
|
+
// them: our MST fork types `isStateTreeNode`'s parameter as a state-tree node
|
|
74
|
+
// (not `unknown`), so `base` (a plain interface) must widen through `unknown`
|
|
75
|
+
// to be passed, and `getSnapshot` of that returns `unknown`.
|
|
76
|
+
const canonicalBaseCache = new WeakMap();
|
|
77
|
+
function toPlainConfig(base) {
|
|
78
|
+
const node = base;
|
|
79
|
+
if (isStateTreeNode(node)) {
|
|
80
|
+
return getSnapshot(node);
|
|
81
|
+
}
|
|
82
|
+
const cached = canonicalBaseCache.get(base);
|
|
83
|
+
if (cached) {
|
|
84
|
+
return cached;
|
|
85
|
+
}
|
|
86
|
+
const schema = pluginManager.pluggableConfigSchemaType('track');
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- tsc7 sees getSnapshot here as unknown (eslint's TS6 service disagrees; see header note)
|
|
88
|
+
/* oxlint-disable typescript/no-unnecessary-type-assertion */
|
|
89
|
+
const hydrated = getSnapshot(schema.create(base, { pluginManager }));
|
|
90
|
+
/* oxlint-enable typescript/no-unnecessary-type-assertion */
|
|
91
|
+
canonicalBaseCache.set(base, hydrated);
|
|
92
|
+
return hydrated;
|
|
93
|
+
}
|
|
5
94
|
return TracksManagerSessionMixin(pluginManager)
|
|
6
95
|
.named('SessionTracksManagerSessionMixin')
|
|
7
96
|
.props({
|
|
8
|
-
|
|
97
|
+
/**
|
|
98
|
+
* #property
|
|
99
|
+
* User-added session tracks (no matching admin config track). A non-admin's
|
|
100
|
+
* *edits* to an existing config track are stored as deltas
|
|
101
|
+
* (trackConfigDeltas), not here.
|
|
102
|
+
*/
|
|
103
|
+
sessionTracks: types.stripDefault(types.array(pluginManager.pluggableConfigSchemaType('track')), []),
|
|
104
|
+
/**
|
|
105
|
+
* #property
|
|
106
|
+
* Per-track config overrides for a non-admin, keyed by trackId, stored as a
|
|
107
|
+
* *delta* against the admin-owned base config (jbrowse.tracks entry) rather
|
|
108
|
+
* than a full copy — so a later admin change to an untouched field still
|
|
109
|
+
* flows through (see trackConfigDelta.ts). Frozen (not a typed track array)
|
|
110
|
+
* on purpose: a typed create() would fill defaults, erasing the "unset vs
|
|
111
|
+
* default" distinction the delta merge relies on.
|
|
112
|
+
*
|
|
113
|
+
* `stripDefault` for the reason every other persisted prop beside it has
|
|
114
|
+
* it: without it the empty map is written into every snapshot and every
|
|
115
|
+
* share link, so a session that never overrode a track still ships
|
|
116
|
+
* `"trackConfigDeltas":{}`. It was the one prop added since that convention
|
|
117
|
+
* without it.
|
|
118
|
+
*/
|
|
119
|
+
trackConfigDeltas: types.stripDefault(types.frozen(), {}),
|
|
9
120
|
})
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
121
|
+
.volatile(() => ({
|
|
122
|
+
/**
|
|
123
|
+
* Per-track private working copies (non-admin), keyed by trackId. A plain
|
|
124
|
+
* Map — not observable, not persisted — mirroring the pluginManager
|
|
125
|
+
* hydration cache: it holds the live MST config node a shown track's
|
|
126
|
+
* in-place quick-edits mutate, so the shared frozen base is never touched.
|
|
127
|
+
* See
|
|
128
|
+
* [ADR-032](https://github.com/GMOD/jbrowse-components/blob/main/agent-docs/architecture-decision-records/adr-032-track-config-nodes-are-throwaway-views.md).
|
|
129
|
+
*
|
|
130
|
+
* Not evicted: it's a pure memoization cache, bounded by the count of
|
|
131
|
+
* distinct tracks shown this session (each entry a lazily-hydrated config
|
|
132
|
+
* node), holding no authoritative state — the persisted delta is the
|
|
133
|
+
* source of truth, and reset/programmatic edits keep a retained copy in
|
|
134
|
+
* sync. Retention is volatile RAM only (never serialized), so it's not
|
|
135
|
+
* worth a reference-counted prune at every track-removal path.
|
|
136
|
+
*
|
|
137
|
+
* Each entry carries the delta it mirrors, so a delta replaced from
|
|
138
|
+
* outside this mixin invalidates it — see `getEditableTrackConfig`.
|
|
139
|
+
*/
|
|
140
|
+
editableTrackConfigs: new Map(),
|
|
14
141
|
}))
|
|
15
|
-
.
|
|
16
|
-
|
|
142
|
+
.views(self => {
|
|
143
|
+
// Memoize merged configs per (base object, delta value) pair so the tracks
|
|
144
|
+
// getter returns stable object identity across unrelated recomputes. A
|
|
145
|
+
// fresh merged object each time would rehydrate a new MST node in
|
|
146
|
+
// TrackConfigurationReference, losing open display state (see CLAUDE.md).
|
|
147
|
+
// Both keys have stable identity until they actually change: a track's
|
|
148
|
+
// delta only when that track is edited, and the base only on a
|
|
149
|
+
// jbrowse.tracks write. This relies on a base config never mutating in
|
|
150
|
+
// place: app-core's frozen array replaces the entry (new identity) on
|
|
151
|
+
// updateTrackConf, and product-core's MST-node bases have no edit path at
|
|
152
|
+
// all (no updateTrackConf; embedded sessions are adminMode:false). If an
|
|
153
|
+
// in-place base edit is ever added, key this cache on base content too.
|
|
154
|
+
const mergeCache = new WeakMap();
|
|
17
155
|
return {
|
|
18
|
-
|
|
156
|
+
/**
|
|
157
|
+
* #getter
|
|
158
|
+
* User-added session tracks first, then each admin config track with its
|
|
159
|
+
* delta (trackConfigDeltas) merged over it. A base track without a delta
|
|
160
|
+
* is returned unchanged by identity to keep the hydration cache warm.
|
|
161
|
+
*/
|
|
162
|
+
get tracks() {
|
|
163
|
+
const deltas = self.trackConfigDeltas;
|
|
164
|
+
const sessionIds = new Set(self.sessionTracks.map(t => t.trackId));
|
|
165
|
+
const configTracks = baseTracks(self);
|
|
166
|
+
const merged = configTracks
|
|
167
|
+
.filter(t => !sessionIds.has(t.trackId))
|
|
168
|
+
.map(base => {
|
|
169
|
+
const delta = deltas[base.trackId];
|
|
170
|
+
if (!delta) {
|
|
171
|
+
return base;
|
|
172
|
+
}
|
|
173
|
+
const cached = mergeCache.get(base);
|
|
174
|
+
if (cached?.delta === delta) {
|
|
175
|
+
return cached.merged;
|
|
176
|
+
}
|
|
177
|
+
const mergedTrack = mergeTrackConfig(toPlainConfig(base), delta);
|
|
178
|
+
mergeCache.set(base, { delta, merged: mergedTrack });
|
|
179
|
+
return mergedTrack;
|
|
180
|
+
});
|
|
181
|
+
return [...self.sessionTracks, ...merged];
|
|
182
|
+
},
|
|
183
|
+
/**
|
|
184
|
+
* #method
|
|
185
|
+
* The overridden slots for `trackId` (empty when it has no delta): each
|
|
186
|
+
* changed setting's path, its base/default value and the edited value.
|
|
187
|
+
* Drives the "view changes" dialog opened from the edited badge.
|
|
188
|
+
*/
|
|
189
|
+
getTrackConfigChanges(trackId) {
|
|
190
|
+
// Every rendered track row asks this (the edited badge), but only an
|
|
191
|
+
// edited track has a delta — so scan for the base only once one
|
|
192
|
+
// exists. Skipping it also keeps an unedited row from subscribing to
|
|
193
|
+
// the whole jbrowse.tracks array.
|
|
194
|
+
const delta = self.trackConfigDeltas[trackId];
|
|
195
|
+
const base = delta
|
|
196
|
+
? baseTracks(self).find(t => t.trackId === trackId)
|
|
197
|
+
: undefined;
|
|
198
|
+
return delta && base
|
|
199
|
+
? flattenTrackConfigDelta(toPlainConfig(base), delta)
|
|
200
|
+
: [];
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* #method
|
|
204
|
+
* A non-admin's private working copy of a track config, created on first
|
|
205
|
+
* access from the current frozen (base+delta) value and cached by
|
|
206
|
+
* trackId, so a shown track's in-place quick-edits (setSlot) mutate this
|
|
207
|
+
* copy and never the shared frozen base node (see ADR-032). Undefined in
|
|
208
|
+
* admin mode — there the base jbrowse.tracks entry is edited in place.
|
|
209
|
+
* Called by TrackConfigurationReference during lazy hydration.
|
|
210
|
+
*
|
|
211
|
+
* Cached against the delta it was built from, not by trackId alone. A
|
|
212
|
+
* delta this mixin wrote re-stamps the entry, so the copy an edit is
|
|
213
|
+
* still being typed into is never swapped out mid-keystroke; a delta
|
|
214
|
+
* replaced from outside — an undo's `applySnapshot` on the session, a
|
|
215
|
+
* session restore — cannot, so the next read rebuilds the copy from the
|
|
216
|
+
* delta that now exists. Reading `trackConfigDeltas` here is also what
|
|
217
|
+
* makes an undo re-resolve the reference at all: the resolver's caller
|
|
218
|
+
* is already subscribed to it through `getTrackById`.
|
|
219
|
+
*/
|
|
220
|
+
getEditableTrackConfig(trackId, frozenConfig, schemaType) {
|
|
19
221
|
if (self.adminMode) {
|
|
20
|
-
return
|
|
222
|
+
return undefined;
|
|
21
223
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
224
|
+
const delta = self.trackConfigDeltas[trackId];
|
|
225
|
+
const existing = self.editableTrackConfigs.get(trackId);
|
|
226
|
+
if (existing && existing.delta === delta) {
|
|
227
|
+
return existing.node;
|
|
25
228
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
229
|
+
const node = schemaType.create(frozenConfig, {
|
|
230
|
+
pluginManager,
|
|
231
|
+
});
|
|
232
|
+
self.editableTrackConfigs.set(trackId, { node, delta });
|
|
233
|
+
return node;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
})
|
|
237
|
+
.actions(self => ({
|
|
238
|
+
afterAttach() {
|
|
239
|
+
// One-time format upgrade: a legacy session stored a non-admin's edits as
|
|
240
|
+
// a full-config sessionTracks entry shadowing the same-id admin track.
|
|
241
|
+
// Convert those to deltas so the whole app uses one override mechanism.
|
|
242
|
+
// Genuinely-added session tracks (no matching base) are left in place.
|
|
243
|
+
const configById = new Map(baseTracks(self).map(t => [t.trackId, t]));
|
|
244
|
+
const legacy = self.sessionTracks.filter(t => configById.has(t.trackId));
|
|
245
|
+
if (legacy.length > 0) {
|
|
246
|
+
const deltas = { ...self.trackConfigDeltas };
|
|
247
|
+
for (const track of legacy) {
|
|
248
|
+
const plainBase = toPlainConfig(configById.get(track.trackId));
|
|
249
|
+
const delta = diffTrackConfig(plainBase, getSnapshot(track));
|
|
250
|
+
// a legacy override identical to its base contributes no changed
|
|
251
|
+
// slots: drop it rather than migrate a content-free delta
|
|
252
|
+
if (deltaHasChanges(plainBase, delta)) {
|
|
253
|
+
deltas[track.trackId] = delta;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
self.trackConfigDeltas = deltas;
|
|
257
|
+
for (const track of legacy) {
|
|
258
|
+
self.sessionTracks.remove(track);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
}))
|
|
263
|
+
.actions(self => {
|
|
264
|
+
const { publishTrackConf: superPublishTrackConf, deleteTrackConf: superDeleteTrackConf, updateTrackConfiguration: superUpdateTrackConfiguration, } = self;
|
|
265
|
+
// A cleared delta reverts the track's live working copy to the base in
|
|
266
|
+
// place, so an open view re-renders to the default. applySnapshot keeps
|
|
267
|
+
// the node identity (existing observers just update); no-op in admin mode
|
|
268
|
+
// or for a track that was never edited (no working copy).
|
|
269
|
+
//
|
|
270
|
+
// Through toPlainConfig like every other read of a base in this file, not
|
|
271
|
+
// the raw entry: in product-core's embedded sessions a base IS a live MST
|
|
272
|
+
// config node, and applySnapshot wants a snapshot — handing it a node
|
|
273
|
+
// reads nested nodes back out where plain objects belong. (For a frozen
|
|
274
|
+
// base the two agree, since applySnapshot re-runs the same
|
|
275
|
+
// preProcessSnapshot that toPlainConfig hydrates through, so this only
|
|
276
|
+
// makes that reliance explicit.)
|
|
277
|
+
function revertEditableTrackConfig(trackId) {
|
|
278
|
+
const entry = self.editableTrackConfigs.get(trackId);
|
|
279
|
+
const base = baseTracks(self).find(t => t.trackId === trackId);
|
|
280
|
+
if (entry && base) {
|
|
281
|
+
applySnapshot(entry.node, toPlainConfig(base));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Re-stamp a working copy with the delta now in trackConfigDeltas, so the
|
|
285
|
+
// copy this mixin just persisted from stays the one the next read
|
|
286
|
+
// resolves. Read back off the prop rather than reusing the written object,
|
|
287
|
+
// so the stamp is whatever `types.frozen` actually stored.
|
|
288
|
+
function stampEditableTrackConfig(trackId) {
|
|
289
|
+
const entry = self.editableTrackConfigs.get(trackId);
|
|
290
|
+
if (entry) {
|
|
291
|
+
entry.delta = self.trackConfigDeltas[trackId];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Single writer for trackConfigDeltas (pass undefined to clear). Clearing
|
|
295
|
+
// also reverts the working copy, which is what makes a Reset visible in an
|
|
296
|
+
// open view — except where the caller says the working copy IS the edit
|
|
297
|
+
// that produced the empty delta (`revertWorkingCopy: false`), where
|
|
298
|
+
// reverting undoes it.
|
|
299
|
+
function writeDelta(trackId, delta, { revertWorkingCopy = true } = {}) {
|
|
300
|
+
self.trackConfigDeltas = delta
|
|
301
|
+
? { ...self.trackConfigDeltas, [trackId]: delta }
|
|
302
|
+
: withoutDelta(self.trackConfigDeltas, trackId);
|
|
303
|
+
if (!delta && revertWorkingCopy) {
|
|
304
|
+
revertEditableTrackConfig(trackId);
|
|
305
|
+
}
|
|
306
|
+
stampEditableTrackConfig(trackId);
|
|
307
|
+
}
|
|
308
|
+
// Whether `trackConf` is the working copy's own current state — i.e. this
|
|
309
|
+
// update came from the track's live `setSlot` edits rather than from the
|
|
310
|
+
// config editor's separate temporary node. `syncEditableTrackConfig` below
|
|
311
|
+
// asks the same question the other way round.
|
|
312
|
+
function isWorkingCopyState(trackId, trackConf) {
|
|
313
|
+
const entry = self.editableTrackConfigs.get(trackId);
|
|
314
|
+
return !!entry && compareStructural(getSnapshot(entry.node), trackConf);
|
|
315
|
+
}
|
|
316
|
+
// Push a *programmatic* update (the config editor's Apply, or any
|
|
317
|
+
// updateTrackConfiguration not driven by this node's own live edits) into
|
|
318
|
+
// the working copy. When the node IS the edit source its snapshot already
|
|
319
|
+
// equals fullConfig, so this skips — which also avoids clobbering an
|
|
320
|
+
// in-progress live drag.
|
|
321
|
+
function syncEditableTrackConfig(trackId, fullConfig) {
|
|
322
|
+
const entry = self.editableTrackConfigs.get(trackId);
|
|
323
|
+
if (entry && !compareStructural(getSnapshot(entry.node), fullConfig)) {
|
|
324
|
+
applySnapshot(entry.node, fullConfig);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// The session-scoped add, shared by the action that always means the
|
|
328
|
+
// session and the one that means it only for a non-admin. A plain closure
|
|
329
|
+
// rather than `this.addSessionTrackConf` so neither action's inferred
|
|
330
|
+
// return type depends on the other's.
|
|
331
|
+
function addToSession(loose) {
|
|
332
|
+
const trackConf = expandLooseTrackConfig(loose, pluginManager);
|
|
333
|
+
const { trackId, type } = trackConf;
|
|
334
|
+
if (!type) {
|
|
335
|
+
throw new Error(`track type not specified for "${trackId}"`);
|
|
336
|
+
}
|
|
337
|
+
assertTrackConfOutlivesItsAssemblies(self, trackConf, 'sessionTracks');
|
|
338
|
+
// Dedupe against everything the session can already resolve — config
|
|
339
|
+
// catalog (jbrowse.tracks), assembly sequences, connection tracks and
|
|
340
|
+
// prior sessionTracks — not just sessionTracks. Re-adding a config
|
|
341
|
+
// already in the catalog would otherwise push a full shadow into
|
|
342
|
+
// sessionTracks, silently demoting a catalog track to a session track
|
|
343
|
+
// and dropping its trackConfigDeltas override semantics.
|
|
344
|
+
const existing = self.getTrackById(trackId);
|
|
345
|
+
if (existing) {
|
|
346
|
+
if (self.sessionTracks.includes(existing)) {
|
|
347
|
+
assertNotReaddedDifferently(pluginManager, existing, {
|
|
348
|
+
...trackConf,
|
|
349
|
+
trackId,
|
|
350
|
+
type,
|
|
351
|
+
});
|
|
29
352
|
}
|
|
353
|
+
return existing;
|
|
354
|
+
}
|
|
355
|
+
// sessionTracks is a typed MST array (unlike the frozen
|
|
356
|
+
// jbrowse.tracks), so an invalid config throws on push. Surface it as
|
|
357
|
+
// a snackbar and skip the add, rather than letting it crash the app.
|
|
358
|
+
try {
|
|
30
359
|
const length = self.sessionTracks.push(trackConf);
|
|
31
360
|
return self.sessionTracks[length - 1];
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
self.notifyError(`Track "${trackId}" has an invalid configuration: ${e}`, e);
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
/**
|
|
369
|
+
* #action
|
|
370
|
+
* Add a track config to *this session*: it lands in `sessionTracks`,
|
|
371
|
+
* travels with the session when it is saved or shared, and never
|
|
372
|
+
* reaches the config.json the server hands every visitor.
|
|
373
|
+
*
|
|
374
|
+
* **The default destination.** Everything that is not an Add-track
|
|
375
|
+
* workflow wants this one, whoever is looking — a session spec's
|
|
376
|
+
* `sessionTracks`, a URL's `&sessionTracks=`, and every track a feature
|
|
377
|
+
* stands up on the user's behalf. Mirrors `addSessionConnectionConf` in
|
|
378
|
+
* the connections mixins.
|
|
379
|
+
*/
|
|
380
|
+
addSessionTrackConf(trackConf) {
|
|
381
|
+
return addToSession(trackConf);
|
|
32
382
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
383
|
+
/**
|
|
384
|
+
* #action
|
|
385
|
+
* Publish a track config to the shared catalog if this user can, and
|
|
386
|
+
* fall back to their session if they cannot: an admin's goes to
|
|
387
|
+
* `jbrowse.tracks`, which the admin server writes back into the
|
|
388
|
+
* config.json every visitor is served; everyone else's goes to
|
|
389
|
+
* `sessionTracks`.
|
|
390
|
+
*
|
|
391
|
+
* **Only the "Add track" workflows.** Those are where an admin adding a
|
|
392
|
+
* track means to add it for the whole site. A track a feature stands up
|
|
393
|
+
* on the user's behalf — a search result, a computed consensus, a
|
|
394
|
+
* reconstruction's segment labels — is not a catalog entry, and one
|
|
395
|
+
* admin click on this publishes it to every visitor, again on the next
|
|
396
|
+
* click, with a per-launch trackId that defeats the dedupe.
|
|
397
|
+
* `addSessionTrackConf` above is the destination for those.
|
|
398
|
+
*
|
|
399
|
+
* An admin's track still goes to the session when it names an assembly
|
|
400
|
+
* the config.json does not carry — a session spec's, a MAF sample's own
|
|
401
|
+
* genome, a comparative view's synthesized pair. The Add-track widget
|
|
402
|
+
* takes its assembly from the containing view, so an admin adding a
|
|
403
|
+
* track while looking at one of those arrives here with a name no
|
|
404
|
+
* visitor can resolve, and publishing it writes an entry the server
|
|
405
|
+
* hands everyone and nobody can draw. The session is the one
|
|
406
|
+
* destination where that track works, so the click keeps working and
|
|
407
|
+
* the dangling entry is never written; a snackbar says which assembly
|
|
408
|
+
* moved it.
|
|
409
|
+
*
|
|
410
|
+
* **The snackbar names the assembly and not the track**, which is what
|
|
411
|
+
* lets a batch collapse to one message: `pushSnackbarMessage` dedupes on
|
|
412
|
+
* the exact text, and `BulkAddTracksWorkflow` publishes in a loop, so
|
|
413
|
+
* interpolating the track name stacked one identical-but-for-a-name
|
|
414
|
+
* toast per file over an admin adding thirty. The assembly is the whole
|
|
415
|
+
* of what an admin has to act on anyway.
|
|
416
|
+
*/
|
|
417
|
+
publishTrackConf(trackConf) {
|
|
418
|
+
if (!self.adminMode) {
|
|
419
|
+
return addToSession(trackConf);
|
|
420
|
+
}
|
|
421
|
+
const missing = assembliesNotInTheCatalog(self, trackConf);
|
|
422
|
+
if (!missing.length) {
|
|
423
|
+
return superPublishTrackConf(trackConf);
|
|
37
424
|
}
|
|
425
|
+
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');
|
|
426
|
+
return addToSession(trackConf);
|
|
427
|
+
},
|
|
428
|
+
/**
|
|
429
|
+
* #action
|
|
430
|
+
* Deprecated alias of `addSessionTrackConf`. Call that, or
|
|
431
|
+
* `publishTrackConf`.
|
|
432
|
+
*
|
|
433
|
+
* @deprecated
|
|
434
|
+
*
|
|
435
|
+
* The session-scoped add under its old name, for the prebuilt plugin
|
|
436
|
+
* bundles that reach it by name at runtime. See the base mixin's copy
|
|
437
|
+
* for why it survives and why it now means the session; nothing in tree
|
|
438
|
+
* may call it.
|
|
439
|
+
*/
|
|
440
|
+
addTrackConf(trackConf) {
|
|
441
|
+
return addToSession(trackConf);
|
|
442
|
+
},
|
|
443
|
+
/**
|
|
444
|
+
* #action
|
|
445
|
+
* Persist a non-admin's edited track config as a delta (trackConfigDeltas)
|
|
446
|
+
* against the admin-owned base — only the changed slots — so the edits
|
|
447
|
+
* persist and are shared while admin changes to untouched fields still
|
|
448
|
+
* flow through. A user-added session track (no base) is edited in place.
|
|
449
|
+
* Everything else (admin edits, opened connection tracks) defers to the
|
|
450
|
+
* base mixin, which routes connection tracks to connectionTrackConfigs and
|
|
451
|
+
* the rest to the jbrowse config.
|
|
452
|
+
*/
|
|
453
|
+
updateTrackConfiguration(trackConf) {
|
|
38
454
|
const { trackId } = trackConf;
|
|
455
|
+
const base = self.adminMode
|
|
456
|
+
? undefined
|
|
457
|
+
: baseTracks(self).find(t => t.trackId === trackId);
|
|
458
|
+
const sessionIdx = self.adminMode
|
|
459
|
+
? -1
|
|
460
|
+
: self.sessionTracks.findIndex(t => t.trackId === trackId);
|
|
461
|
+
if (base) {
|
|
462
|
+
const plainBase = toPlainConfig(base);
|
|
463
|
+
const delta = diffTrackConfig(plainBase, trackConf);
|
|
464
|
+
// an edit that nets back to the base carries no changed slots: clear
|
|
465
|
+
// any prior override (implicit reset) instead of pinning a
|
|
466
|
+
// content-free delta; skip a no-op write when there's nothing to
|
|
467
|
+
// clear, so the tracks getter doesn't needlessly churn identity
|
|
468
|
+
if (deltaHasChanges(plainBase, delta)) {
|
|
469
|
+
// Two views showing the same track each run BaseTrackModel's
|
|
470
|
+
// persist reaction against the shared config node, so a single edit
|
|
471
|
+
// calls this twice with an identical delta (and the config editor
|
|
472
|
+
// can re-save an unchanged config). Skip a structurally-identical
|
|
473
|
+
// re-store: writing a fresh trackConfigDeltas object would churn its
|
|
474
|
+
// identity and make the tracks getter rehydrate a new merged node
|
|
475
|
+
// for no real change.
|
|
476
|
+
const existing = self.trackConfigDeltas[trackId];
|
|
477
|
+
if (!existing || !compareStructural(existing, delta)) {
|
|
478
|
+
writeDelta(trackId, delta);
|
|
479
|
+
}
|
|
480
|
+
syncEditableTrackConfig(trackId, trackConf);
|
|
481
|
+
}
|
|
482
|
+
else if (trackId in self.trackConfigDeltas) {
|
|
483
|
+
// An empty delta is two different situations and only one of them
|
|
484
|
+
// is a reset. `diffTrackConfig` records adds and changes, never
|
|
485
|
+
// deletions (its module note), so *unsetting* a slot the admin
|
|
486
|
+
// base sets diffs to nothing exactly as netting back to the base
|
|
487
|
+
// does — and reverting the working copy then undid the edit ~400ms
|
|
488
|
+
// after the user watched it land. The reachable case is a size
|
|
489
|
+
// row's reset, whose whole job is to unset a slot, over a
|
|
490
|
+
// promotable slot an admin `config.json` declares.
|
|
491
|
+
//
|
|
492
|
+
// The removal still doesn't survive a reload, which is the
|
|
493
|
+
// tombstone-free delta's documented limitation; what it no longer
|
|
494
|
+
// does is undo itself on screen. Only the working-copy-driven path
|
|
495
|
+
// skips the revert: the config editor edits a separate temporary
|
|
496
|
+
// node, so there the revert is the only thing that moves the open
|
|
497
|
+
// track back to the base.
|
|
498
|
+
writeDelta(trackId, undefined, {
|
|
499
|
+
revertWorkingCopy: !isWorkingCopyState(trackId, trackConf),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else if (sessionIdx !== -1) {
|
|
504
|
+
// a user-added session track (no admin base): edit it in place. A
|
|
505
|
+
// typed MST array throws on an invalid config — snackbar it.
|
|
506
|
+
try {
|
|
507
|
+
self.sessionTracks[sessionIdx] = trackConf;
|
|
508
|
+
}
|
|
509
|
+
catch (e) {
|
|
510
|
+
self.notifyError(`Track "${trackId}" has an invalid configuration: ${e}`, e);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
// admin edit, or a track with no admin base / sessionTracks entry
|
|
515
|
+
// (an opened connection track, or a homeless in-memory-only edit):
|
|
516
|
+
// the base mixin routes these
|
|
517
|
+
superUpdateTrackConfiguration(trackConf);
|
|
518
|
+
// An admin's edit rewrites the base config itself, so it supersedes
|
|
519
|
+
// any delta: a shared session authored by a non-admin carries their
|
|
520
|
+
// deltas, and an admin opening it edits jbrowse.tracks directly. Left
|
|
521
|
+
// in place, the delta merges straight back over that base in the
|
|
522
|
+
// `tracks` getter and the admin's own edit silently reverts.
|
|
523
|
+
if (self.adminMode && trackId in self.trackConfigDeltas) {
|
|
524
|
+
writeDelta(trackId, undefined);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
/**
|
|
529
|
+
* #action
|
|
530
|
+
* Drop a non-admin's delta (trackConfigDeltas) so the track reverts to
|
|
531
|
+
* its admin config (jbrowse.tracks) default. Unlike deleteTrackConf this
|
|
532
|
+
* does not dereference the track from open views — the base config
|
|
533
|
+
* re-resolves in place, so an open track stays open and simply reverts.
|
|
534
|
+
*/
|
|
535
|
+
resetTrackConfiguration(trackId) {
|
|
536
|
+
if (trackId in self.trackConfigDeltas) {
|
|
537
|
+
writeDelta(trackId, undefined);
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
/**
|
|
541
|
+
* #action
|
|
542
|
+
*/
|
|
543
|
+
deleteTrackConf(trackConf) {
|
|
544
|
+
superDeleteTrackConf(trackConf);
|
|
545
|
+
const { trackId } = trackConf;
|
|
546
|
+
// A delta only outlives its base if the base is gone, so drop it here
|
|
547
|
+
// rather than strand it. Reachable only programmatically (the UI offers
|
|
548
|
+
// a non-admin Reset, not Delete, for a delta-bearing base track).
|
|
549
|
+
if (trackId in self.trackConfigDeltas) {
|
|
550
|
+
writeDelta(trackId, undefined);
|
|
551
|
+
}
|
|
39
552
|
const idx = self.sessionTracks.findIndex(t => t.trackId === trackId);
|
|
40
553
|
if (idx === -1) {
|
|
41
554
|
return undefined;
|
|
@@ -43,18 +556,10 @@ export function SessionTracksManagerSessionMixin(pluginManager) {
|
|
|
43
556
|
return self.sessionTracks.splice(idx, 1);
|
|
44
557
|
},
|
|
45
558
|
};
|
|
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
559
|
});
|
|
57
560
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
561
|
+
// The `isSessionWithSessionTracks` guard that used to live here now lives in
|
|
562
|
+
// `@jbrowse/core/util/types` beside the rest of the session capability guards.
|
|
563
|
+
// Its only caller was the hierarchical track selector — a plugin, which could
|
|
564
|
+
// not reach product-core through the ABI it publishes against, so this package
|
|
565
|
+
// was a runtime dependency of two shipped plugins for three type guards.
|