@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
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { pluggableElementTypeGroups } from '@jbrowse/core/PluginManager';
|
|
2
|
+
import { getPropertyMembers, getUnionSubtypes, isArrayType, isMapType, isModelType, isReferenceType, isType, isUnionType, } from '@jbrowse/mobx-state-tree';
|
|
3
|
+
import { isRecord } from './snapshotUtils.js';
|
|
4
|
+
const HELD_NODES_KEY = 'heldForMissingPlugins';
|
|
5
|
+
// The registered elements a `pluggableMstType` union is built from. Mirrors
|
|
6
|
+
// that function's own filter: a registered type whose `stateModel` is not a
|
|
7
|
+
// model type is left out of the union, so a snapshot naming it fails the same
|
|
8
|
+
// way one naming nothing at all does.
|
|
9
|
+
//
|
|
10
|
+
// Typed structurally rather than as `PluggableElementBase`, which is not a
|
|
11
|
+
// published `@jbrowse/core` subpath — adding one just to name a type here would
|
|
12
|
+
// widen the plugin ABI for nothing.
|
|
13
|
+
function buildableElements(pluginManager, group) {
|
|
14
|
+
return pluginManager.getElementTypesInGroup(group).filter(t => {
|
|
15
|
+
const { stateModel } = t;
|
|
16
|
+
return isType(stateModel) && isModelType(stateModel);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
// optional/stripDefault/late/snapshotProcessor all report the type they wrap,
|
|
20
|
+
// so one loop reaches the array, map, model or union a property really declares.
|
|
21
|
+
function unwrapType(type) {
|
|
22
|
+
let current = type;
|
|
23
|
+
for (let i = 0; i < 16; i++) {
|
|
24
|
+
const sub = current.getSubTypes();
|
|
25
|
+
if (!sub ||
|
|
26
|
+
typeof sub !== 'object' ||
|
|
27
|
+
Array.isArray(sub) ||
|
|
28
|
+
sub === current) {
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
current = sub;
|
|
32
|
+
}
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
function containerType(type) {
|
|
36
|
+
if (isUnionType(type)) {
|
|
37
|
+
for (const member of getUnionSubtypes(type)) {
|
|
38
|
+
const found = containerType(unwrapType(member));
|
|
39
|
+
if (found) {
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return isArrayType(type) || isMapType(type) ? type : undefined;
|
|
46
|
+
}
|
|
47
|
+
// One prune's view of the plugin manager. The unions are built on first use, so
|
|
48
|
+
// a session naming only registered types — every session this build produced
|
|
49
|
+
// itself — never constructs one.
|
|
50
|
+
class Registry {
|
|
51
|
+
pluginManager;
|
|
52
|
+
names = new Map();
|
|
53
|
+
aliases = new Map();
|
|
54
|
+
unions = new Map();
|
|
55
|
+
groups;
|
|
56
|
+
discriminants = new Map();
|
|
57
|
+
constructor(pluginManager) {
|
|
58
|
+
this.pluginManager = pluginManager;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The group a container holds, or undefined for a container of anything else.
|
|
62
|
+
* Identified by member identity rather than by the union object, which
|
|
63
|
+
* `pluggableMstType` rebuilds on every call: the members *are* the registered
|
|
64
|
+
* `stateModel`s.
|
|
65
|
+
*/
|
|
66
|
+
groupOf(elementType) {
|
|
67
|
+
if (isReferenceType(elementType) || !isUnionType(elementType)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
this.groups ??= new Map(pluggableElementTypeGroups.flatMap(group => buildableElements(this.pluginManager, group).map(t => [t.stateModel, group])));
|
|
71
|
+
for (const member of getUnionSubtypes(elementType)) {
|
|
72
|
+
const group = this.groups.get(member);
|
|
73
|
+
if (group) {
|
|
74
|
+
return group;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The name this build registers the node's type under — itself, or the
|
|
81
|
+
* element that declares it as an alias — or undefined if neither does.
|
|
82
|
+
*/
|
|
83
|
+
canonicalName(group, type) {
|
|
84
|
+
return this.namesFor(group).has(type)
|
|
85
|
+
? type
|
|
86
|
+
: this.aliasesFor(group).get(type);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The union member a node's type names, so the walk can descend into what
|
|
90
|
+
* that concrete model declares. A type the registry cannot name has no
|
|
91
|
+
* member to find; the node is then judged whole by `accepts`.
|
|
92
|
+
*/
|
|
93
|
+
memberFor(elementType, type) {
|
|
94
|
+
for (const member of getUnionSubtypes(elementType)) {
|
|
95
|
+
if (this.discriminantOf(member) === type) {
|
|
96
|
+
return member;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/** Whether the real MST union takes this snapshot. */
|
|
102
|
+
accepts(node, group) {
|
|
103
|
+
let union = this.unions.get(group);
|
|
104
|
+
if (!union) {
|
|
105
|
+
union = this.pluginManager.pluggableMstType(group, 'stateModel');
|
|
106
|
+
this.unions.set(group, union);
|
|
107
|
+
}
|
|
108
|
+
return union.is(node);
|
|
109
|
+
}
|
|
110
|
+
discriminantOf(member) {
|
|
111
|
+
if (this.discriminants.has(member)) {
|
|
112
|
+
return this.discriminants.get(member);
|
|
113
|
+
}
|
|
114
|
+
const literal = isModelType(member)
|
|
115
|
+
? getPropertyMembers(member).properties.type
|
|
116
|
+
: undefined;
|
|
117
|
+
const value = typeof literal?.value === 'string' ? literal.value : undefined;
|
|
118
|
+
this.discriminants.set(member, value);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
namesFor(group) {
|
|
122
|
+
let names = this.names.get(group);
|
|
123
|
+
if (!names) {
|
|
124
|
+
names = new Set(buildableElements(this.pluginManager, group).map(t => t.name));
|
|
125
|
+
this.names.set(group, names);
|
|
126
|
+
}
|
|
127
|
+
return names;
|
|
128
|
+
}
|
|
129
|
+
aliasesFor(group) {
|
|
130
|
+
let aliases = this.aliases.get(group);
|
|
131
|
+
if (!aliases) {
|
|
132
|
+
aliases = new Map(buildableElements(this.pluginManager, group).flatMap(t => t.aliases?.map(alias => [alias, t.name]) ?? []));
|
|
133
|
+
this.aliases.set(group, aliases);
|
|
134
|
+
}
|
|
135
|
+
return aliases;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function describe(node, group) {
|
|
139
|
+
const type = isRecord(node) ? node.type : undefined;
|
|
140
|
+
return { group, type: typeof type === 'string' ? type : 'unknown' };
|
|
141
|
+
}
|
|
142
|
+
function readId(node) {
|
|
143
|
+
const id = isRecord(node) ? node.id : undefined;
|
|
144
|
+
return typeof id === 'string' ? id : undefined;
|
|
145
|
+
}
|
|
146
|
+
function readHeld(snapshot) {
|
|
147
|
+
const held = snapshot[HELD_NODES_KEY];
|
|
148
|
+
return Array.isArray(held) ? held : [];
|
|
149
|
+
}
|
|
150
|
+
function anchorKey(group, parent) {
|
|
151
|
+
return `${group}/${parent ?? ''}`;
|
|
152
|
+
}
|
|
153
|
+
function collectIds(value, into) {
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
for (const item of value) {
|
|
156
|
+
collectIds(item, into);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (isRecord(value)) {
|
|
160
|
+
if (typeof value.id === 'string') {
|
|
161
|
+
into.add(value.id);
|
|
162
|
+
}
|
|
163
|
+
for (const child of Object.values(value)) {
|
|
164
|
+
collectIds(child, into);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
class Walk {
|
|
169
|
+
registry;
|
|
170
|
+
dropped = [];
|
|
171
|
+
held = [];
|
|
172
|
+
/** ids of nodes taken out, so a reference container can drop the danglers */
|
|
173
|
+
removed = new Set();
|
|
174
|
+
pending = new Map();
|
|
175
|
+
// a node restored on this same call is walked like any other, so its own
|
|
176
|
+
// unbuildable children come out again — but the user is told nothing, or a
|
|
177
|
+
// held track whose displays are still missing warns on every load forever.
|
|
178
|
+
silence = 0;
|
|
179
|
+
emptied = 0;
|
|
180
|
+
constructor(registry) {
|
|
181
|
+
this.registry = registry;
|
|
182
|
+
}
|
|
183
|
+
// counted rather than flagged, so the track that owns a display container
|
|
184
|
+
// reads a difference across its own walk and needs no save-and-restore
|
|
185
|
+
emptiedDisplays() {
|
|
186
|
+
this.emptied++;
|
|
187
|
+
}
|
|
188
|
+
emptiedDisplayCount() {
|
|
189
|
+
return this.emptied;
|
|
190
|
+
}
|
|
191
|
+
offer(entry) {
|
|
192
|
+
const key = anchorKey(entry.held.group, entry.held.parent);
|
|
193
|
+
const list = this.pending.get(key);
|
|
194
|
+
if (list) {
|
|
195
|
+
list.push(entry);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
this.pending.set(key, [entry]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
take(group, parent) {
|
|
202
|
+
const key = anchorKey(group, parent);
|
|
203
|
+
const taken = this.pending.get(key);
|
|
204
|
+
this.pending.delete(key);
|
|
205
|
+
return taken ?? [];
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Entries no container claimed. A node whose subtree the walk descended into
|
|
209
|
+
* is one the registry can name, and so one it admits — the single exception
|
|
210
|
+
* is a track the cascade takes, which needs its display list to have come out
|
|
211
|
+
* empty and so to have claimed nothing. Nothing a container takes can
|
|
212
|
+
* therefore be discarded with the node around it.
|
|
213
|
+
*/
|
|
214
|
+
unplaced() {
|
|
215
|
+
return new Set([...this.pending.values()].flat().map(p => p.held));
|
|
216
|
+
}
|
|
217
|
+
drop(node) {
|
|
218
|
+
if (this.silence === 0) {
|
|
219
|
+
this.dropped.push(node);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
quietly(quiet, run) {
|
|
223
|
+
this.silence += quiet ? 1 : 0;
|
|
224
|
+
try {
|
|
225
|
+
return run();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
this.silence -= quiet ? 1 : 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Children first, then the node itself: the union's `is` validates a whole
|
|
234
|
+
* subtree, so a view still holding an unbuildable track would be refused as a
|
|
235
|
+
* whole and taken down with it. Returns undefined for a node this build cannot
|
|
236
|
+
* hold, which the container then holds whole and unpruned.
|
|
237
|
+
*/
|
|
238
|
+
function pruneNode(node, group, elementType, parent, walk) {
|
|
239
|
+
if (!isRecord(node)) {
|
|
240
|
+
// MST reports a node with no readable type precisely; guessing at a repair
|
|
241
|
+
// here would hide it
|
|
242
|
+
return node;
|
|
243
|
+
}
|
|
244
|
+
const type = node.type;
|
|
245
|
+
if (typeof type !== 'string') {
|
|
246
|
+
return node;
|
|
247
|
+
}
|
|
248
|
+
const canonical = walk.registry.canonicalName(group, type);
|
|
249
|
+
const named = canonical === undefined || canonical === type
|
|
250
|
+
? node
|
|
251
|
+
: { ...node, type: canonical };
|
|
252
|
+
// only for a name the registry knows, which is exactly the set `accepts`
|
|
253
|
+
// waves through: a node whose subtree the walk descends into is one it can
|
|
254
|
+
// put back
|
|
255
|
+
const model = canonical === undefined
|
|
256
|
+
? undefined
|
|
257
|
+
: walk.registry.memberFor(elementType, canonical);
|
|
258
|
+
const emptied = walk.emptiedDisplayCount();
|
|
259
|
+
const pruned = model
|
|
260
|
+
? walkModel(model, named, readId(named) ?? parent, walk)
|
|
261
|
+
: named;
|
|
262
|
+
const cascaded = group === 'track' && walk.emptiedDisplayCount() > emptied;
|
|
263
|
+
// A display type can go missing while its track type stays — a plugin
|
|
264
|
+
// contributing one more display to an existing track type is the ordinary
|
|
265
|
+
// case. A track left with none of them goes too: an empty `displays` array is
|
|
266
|
+
// valid MST, so nothing downstream would refuse it, and `activeDisplay` reads
|
|
267
|
+
// `displays[0]` on the promise that a shown track always has one.
|
|
268
|
+
if (cascaded) {
|
|
269
|
+
walk.drop({ ...describe(node, group), cascade: true });
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
// a registered name is admitted without asking the union, so a **malformed**
|
|
273
|
+
// snapshot of a type this build does have still reaches MST and throws there
|
|
274
|
+
if (canonical !== undefined || walk.registry.accepts(pruned, group)) {
|
|
275
|
+
return pruned;
|
|
276
|
+
}
|
|
277
|
+
walk.drop(describe(node, group));
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
// Returns the same list by identity when nothing moved, so a session this build
|
|
281
|
+
// produced itself comes back out of the whole walk as the object that went in.
|
|
282
|
+
function pruneArray(value, group, elementType, parent, walk) {
|
|
283
|
+
const list = Array.isArray(value) ? value : undefined;
|
|
284
|
+
const restoring = walk.take(group, parent);
|
|
285
|
+
if (!list && restoring.length === 0) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
// ascending index, so a run of restores into one list lands in the order it
|
|
289
|
+
// came out of it
|
|
290
|
+
const merged = (list ?? []).map(node => ({ node, restored: false }));
|
|
291
|
+
for (const entry of restoring.sort((a, b) => (a.held.index ?? 0) - (b.held.index ?? 0))) {
|
|
292
|
+
merged.splice(Math.min(entry.held.index ?? merged.length, merged.length), 0, {
|
|
293
|
+
node: entry.snapshot,
|
|
294
|
+
restored: true,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const out = [];
|
|
298
|
+
let changed = merged.length !== (list?.length ?? 0);
|
|
299
|
+
for (const [index, { node, restored }] of merged.entries()) {
|
|
300
|
+
const mark = walk.held.length;
|
|
301
|
+
const pruned = walk.quietly(restored, () => pruneNode(node, group, elementType, parent, walk));
|
|
302
|
+
if (pruned !== undefined || !isRecord(node)) {
|
|
303
|
+
changed ||= pruned !== node;
|
|
304
|
+
out.push(pruned);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// held whole and unpruned, and the entries its own children produced are
|
|
308
|
+
// rolled back — so a dropped view's tracks are inside the copy of the view,
|
|
309
|
+
// not separate entries anchored to a view that is no longer in the tree
|
|
310
|
+
walk.held.length = mark;
|
|
311
|
+
walk.held.push({ group, parent, index, snapshot: node });
|
|
312
|
+
const id = readId(node);
|
|
313
|
+
if (id !== undefined) {
|
|
314
|
+
walk.removed.add(id);
|
|
315
|
+
}
|
|
316
|
+
changed = true;
|
|
317
|
+
}
|
|
318
|
+
// read by the track this container belongs to: a track left with none of its
|
|
319
|
+
// displays goes too
|
|
320
|
+
if (group === 'display' && out.length === 0 && (list?.length ?? 0) > 0) {
|
|
321
|
+
walk.emptiedDisplays();
|
|
322
|
+
}
|
|
323
|
+
return changed || !list ? out : list;
|
|
324
|
+
}
|
|
325
|
+
function pruneMap(value, group, elementType, parent, walk) {
|
|
326
|
+
const map = isRecord(value) ? value : undefined;
|
|
327
|
+
const restoring = walk.take(group, parent);
|
|
328
|
+
if (!map && restoring.length === 0) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const out = {};
|
|
332
|
+
let changed = false;
|
|
333
|
+
for (const [key, node] of Object.entries(map ?? {})) {
|
|
334
|
+
const mark = walk.held.length;
|
|
335
|
+
const pruned = pruneNode(node, group, elementType, parent, walk);
|
|
336
|
+
if (pruned !== undefined || !isRecord(node)) {
|
|
337
|
+
changed ||= pruned !== node;
|
|
338
|
+
out[key] = pruned;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
walk.held.length = mark;
|
|
342
|
+
walk.held.push({ group, key, parent, snapshot: node });
|
|
343
|
+
walk.removed.add(readId(node) ?? key);
|
|
344
|
+
changed = true;
|
|
345
|
+
}
|
|
346
|
+
for (const entry of restoring) {
|
|
347
|
+
const key = entry.held.key;
|
|
348
|
+
if (key === undefined) {
|
|
349
|
+
// `take` has already claimed it, and a map places by key alone — so
|
|
350
|
+
// holding it again is the only thing here that is not a silent delete
|
|
351
|
+
walk.held.push(entry.held);
|
|
352
|
+
changed = true;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const mark = walk.held.length;
|
|
356
|
+
const pruned = walk.quietly(true, () => pruneNode(entry.snapshot, group, elementType, parent, walk));
|
|
357
|
+
if (pruned === undefined) {
|
|
358
|
+
walk.held.length = mark;
|
|
359
|
+
walk.held.push({ group, key, parent, snapshot: entry.snapshot });
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
out[key] = pruned;
|
|
363
|
+
}
|
|
364
|
+
changed = true;
|
|
365
|
+
}
|
|
366
|
+
return changed || !map ? out : map;
|
|
367
|
+
}
|
|
368
|
+
function pruneContainer(type, elementType, group, value, parent, walk) {
|
|
369
|
+
return isMapType(type)
|
|
370
|
+
? pruneMap(value, group, elementType, parent, walk)
|
|
371
|
+
: pruneArray(value, group, elementType, parent, walk);
|
|
372
|
+
}
|
|
373
|
+
// The model a record's snapshot is an instance of. A union that is not a
|
|
374
|
+
// pluggable one — `types.maybe(SomeModel)` — still has to resolve to the member
|
|
375
|
+
// the snapshot names, or the walk stops at a node whose children it could have
|
|
376
|
+
// reached.
|
|
377
|
+
function concreteModel(type, value) {
|
|
378
|
+
if (isUnionType(type)) {
|
|
379
|
+
const members = getUnionSubtypes(type).filter(m => isModelType(m) && !isUnionType(m));
|
|
380
|
+
if (members.length === 1) {
|
|
381
|
+
return members[0];
|
|
382
|
+
}
|
|
383
|
+
return members.find(m => getPropertyMembers(m).properties.type?.value === value.type);
|
|
384
|
+
}
|
|
385
|
+
return isModelType(type) ? type : undefined;
|
|
386
|
+
}
|
|
387
|
+
function walkValue(declared, value, parent, walk) {
|
|
388
|
+
const type = unwrapType(declared);
|
|
389
|
+
const container = containerType(type);
|
|
390
|
+
if (container) {
|
|
391
|
+
const elementType = unwrapType(container.getChildType());
|
|
392
|
+
const group = walk.registry.groupOf(elementType);
|
|
393
|
+
if (group) {
|
|
394
|
+
return (pruneContainer(container, elementType, group, value, parent, walk) ??
|
|
395
|
+
value);
|
|
396
|
+
}
|
|
397
|
+
if (Array.isArray(value)) {
|
|
398
|
+
const out = value.map(item => walkValue(elementType, item, parent, walk));
|
|
399
|
+
return out.some((item, i) => item !== value[i]) ? out : value;
|
|
400
|
+
}
|
|
401
|
+
if (isRecord(value)) {
|
|
402
|
+
const entries = Object.entries(value).map(([key, item]) => [key, walkValue(elementType, item, parent, walk)]);
|
|
403
|
+
return entries.some(([key, item]) => item !== value[key])
|
|
404
|
+
? Object.fromEntries(entries)
|
|
405
|
+
: value;
|
|
406
|
+
}
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
if (isRecord(value)) {
|
|
410
|
+
const model = concreteModel(type, value);
|
|
411
|
+
if (model) {
|
|
412
|
+
return walkModel(model, value, readId(value) ?? parent, walk);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Every property the model declares, not every key the snapshot has: a
|
|
419
|
+
* container whose key MST stripped — `widgets` is `stripDefault(map(...), {})`,
|
|
420
|
+
* so an empty drawer has no key at all — still has to be reached, or a held
|
|
421
|
+
* widget has nowhere to come back to.
|
|
422
|
+
*/
|
|
423
|
+
function walkModel(model, node, parent, walk) {
|
|
424
|
+
const { properties } = getPropertyMembers(model);
|
|
425
|
+
let out = node;
|
|
426
|
+
const write = (key, value) => {
|
|
427
|
+
if (out === node) {
|
|
428
|
+
out = { ...node };
|
|
429
|
+
}
|
|
430
|
+
out[key] = value;
|
|
431
|
+
};
|
|
432
|
+
const references = [];
|
|
433
|
+
for (const [key, declared] of Object.entries(properties)) {
|
|
434
|
+
const type = unwrapType(declared);
|
|
435
|
+
const container = containerType(type);
|
|
436
|
+
if (container) {
|
|
437
|
+
const elementType = unwrapType(container.getChildType());
|
|
438
|
+
if (isReferenceType(elementType)) {
|
|
439
|
+
references.push(key);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const group = walk.registry.groupOf(elementType);
|
|
443
|
+
if (group) {
|
|
444
|
+
const next = pruneContainer(container, elementType, group, node[key], parent, walk);
|
|
445
|
+
if (next !== undefined && next !== node[key]) {
|
|
446
|
+
write(key, next);
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const value = node[key];
|
|
452
|
+
if (value === undefined) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const next = walkValue(type, value, parent, walk);
|
|
456
|
+
if (next !== value) {
|
|
457
|
+
write(key, next);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// After the value properties, so a reference into a sibling container sees
|
|
461
|
+
// what that container dropped. `activeWidgets` holds safeReferences, which do
|
|
462
|
+
// drop themselves when their target is gone, but only once something resolves
|
|
463
|
+
// them — clearing them here keeps the pruned snapshot internally consistent
|
|
464
|
+
// rather than relying on that. Deliberately not restored alongside their
|
|
465
|
+
// target: a drawer panel that comes back closed costs a click, where a widget
|
|
466
|
+
// that comes back missing costs the work in it.
|
|
467
|
+
for (const key of references) {
|
|
468
|
+
const value = out[key];
|
|
469
|
+
if (!isRecord(value) || walk.removed.size === 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const entries = Object.entries(value).filter(([, id]) => typeof id !== 'string' || !walk.removed.has(id));
|
|
473
|
+
if (entries.length !== Object.keys(value).length) {
|
|
474
|
+
write(key, Object.fromEntries(entries));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Takes the session-snapshot nodes whose pluggable type this plugin manager
|
|
481
|
+
* cannot build out of the tree and holds them under `heldForMissingPlugins`,
|
|
482
|
+
* and puts back the ones already held that it now can build — in one walk of
|
|
483
|
+
* the session type, so the two can never disagree about where a node lives.
|
|
484
|
+
* Returns the snapshot unchanged (by identity) when there is nothing to do
|
|
485
|
+
* either way, which is every session this build produced itself.
|
|
486
|
+
*/
|
|
487
|
+
export function pruneUnbuildableNodes(snapshot, pluginManager, sessionModelType) {
|
|
488
|
+
const registry = new Registry(pluginManager);
|
|
489
|
+
const model = concreteModel(unwrapType(sessionModelType), snapshot);
|
|
490
|
+
if (!model) {
|
|
491
|
+
return { snapshot, dropped: [] };
|
|
492
|
+
}
|
|
493
|
+
const walk = new Walk(registry);
|
|
494
|
+
const incoming = readHeld(snapshot);
|
|
495
|
+
const restorable = new Set();
|
|
496
|
+
for (const entry of incoming) {
|
|
497
|
+
// canonicalized on the way back in, so a held node whose type has since
|
|
498
|
+
// been renamed comes back under the name this build registers
|
|
499
|
+
const type = isRecord(entry.snapshot) ? entry.snapshot.type : undefined;
|
|
500
|
+
if (typeof type !== 'string') {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const canonical = registry.canonicalName(entry.group, type);
|
|
504
|
+
if (canonical === undefined &&
|
|
505
|
+
!registry.accepts(entry.snapshot, entry.group)) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
restorable.add(entry);
|
|
509
|
+
walk.offer({
|
|
510
|
+
held: entry,
|
|
511
|
+
snapshot: canonical === undefined || canonical === type
|
|
512
|
+
? entry.snapshot
|
|
513
|
+
: { ...entry.snapshot, type: canonical },
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const pruned = walkModel(model, snapshot, undefined, walk);
|
|
517
|
+
// An entry whose container the recipient deleted is collected here rather than
|
|
518
|
+
// riding along in every autosave and every share link forever — `HeldNode`
|
|
519
|
+
// says such a node is not restorable and is not meant to be.
|
|
520
|
+
//
|
|
521
|
+
// The test is that the container is GONE, never that the walk failed to reach
|
|
522
|
+
// the anchor. `ElementId` is `types.optional(types.identifier, …)`, so a
|
|
523
|
+
// container the session was authored without an id for is anchored by its
|
|
524
|
+
// parent instead, MST mints it one, and the autosave then answers a different
|
|
525
|
+
// key — which reads as a deletion and loses the node the message just promised
|
|
526
|
+
// back. A container that is itself held counts as present: its snapshot is in
|
|
527
|
+
// this session too, just not in the tree.
|
|
528
|
+
const presentIds = new Set();
|
|
529
|
+
if (incoming.length > 0) {
|
|
530
|
+
collectIds(pruned, presentIds);
|
|
531
|
+
for (const entry of walk.held) {
|
|
532
|
+
collectIds(entry.snapshot, presentIds);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const unplaced = walk.unplaced();
|
|
536
|
+
const stillHeld = incoming.filter(entry => (!restorable.has(entry) || unplaced.has(entry)) &&
|
|
537
|
+
(entry.parent === undefined || presentIds.has(entry.parent)));
|
|
538
|
+
const held = [...stillHeld, ...walk.held];
|
|
539
|
+
if (pruned === snapshot &&
|
|
540
|
+
walk.held.length === 0 &&
|
|
541
|
+
stillHeld.length === incoming.length) {
|
|
542
|
+
return { snapshot, dropped: walk.dropped };
|
|
543
|
+
}
|
|
544
|
+
const out = { ...pruned };
|
|
545
|
+
if (held.length > 0) {
|
|
546
|
+
out[HELD_NODES_KEY] = held;
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
delete out[HELD_NODES_KEY];
|
|
550
|
+
}
|
|
551
|
+
return { snapshot: out, dropped: walk.dropped };
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The user-facing summary of one prune, or undefined when nothing was dropped.
|
|
555
|
+
* Names the missing types rather than counting nodes: the plugin a person has to
|
|
556
|
+
* install is the actionable half, and one missing display type can take several
|
|
557
|
+
* nodes with it. Says what a person can do about it, because the nodes are not
|
|
558
|
+
* gone — they ride along under `heldForMissingPlugins` and come back the moment
|
|
559
|
+
* a build that has the plugin opens the session.
|
|
560
|
+
*/
|
|
561
|
+
export function describeUnbuildableNodes(dropped) {
|
|
562
|
+
const types = [
|
|
563
|
+
...new Set(dropped.flatMap(d => (d.cascade ? [] : [d.type]))),
|
|
564
|
+
].join(', ');
|
|
565
|
+
return types
|
|
566
|
+
? `Kept but not shown, pending plugins this JBrowse does not have: ${types}. Install them and reload to get them back.`
|
|
567
|
+
: undefined;
|
|
568
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type AssemblyConfig = Record<string, unknown>;
|
|
2
|
+
export interface TextSearchAdapterConfig {
|
|
3
|
+
textSearchAdapterId: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
type SearchAdapters = TextSearchAdapterConfig[] | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The shapes an assembly can take, discriminated at resolve time: a sequence
|
|
9
|
+
* file URL (`'.../hg38.fa.gz'`, `.2bit`, ...) built into an assembly via
|
|
10
|
+
* `makeAssembly`; a hub name (`'hg38'`, `'GCF_...'`) fetched from jbrowse.org;
|
|
11
|
+
* a full hub config (as `fetchHub` returns); or a bare assembly config (e.g.
|
|
12
|
+
* from `makeAssembly`) — the latter two both being plain config objects.
|
|
13
|
+
*/
|
|
14
|
+
export type AssemblyInput = string | AssemblyConfig;
|
|
15
|
+
export interface ResolvedAssembly {
|
|
16
|
+
assembly: AssemblyConfig;
|
|
17
|
+
/** whatever search adapters the hub this came from carried */
|
|
18
|
+
aggregateTextSearchAdapters?: SearchAdapters;
|
|
19
|
+
}
|
|
20
|
+
export interface ResolvedAssemblies {
|
|
21
|
+
assemblies: AssemblyConfig[];
|
|
22
|
+
/** whatever search adapters the resolved hubs carried, merged */
|
|
23
|
+
aggregateTextSearchAdapters?: SearchAdapters;
|
|
24
|
+
}
|
|
25
|
+
/** One assembly in any of the four accepted shapes. */
|
|
26
|
+
export declare function resolveAssembly(input: AssemblyInput): Promise<ResolvedAssembly>;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a whole list of assembly inputs, so a multi-assembly product takes
|
|
29
|
+
* the same vocabulary the single-view one does — `['hg38', 'mm39']` as readily
|
|
30
|
+
* as two hand-written configs.
|
|
31
|
+
*
|
|
32
|
+
* Exported because the resolution is async and `createApp` is not: an engine
|
|
33
|
+
* that resolved its own genomes would have to hand back a controller whose
|
|
34
|
+
* `viewState` is undefined until the fetches land, which is most of what makes
|
|
35
|
+
* the single-view controller four times the size. A host builds its options
|
|
36
|
+
* asynchronously already (runtime plugins), so this belongs in that step.
|
|
37
|
+
*
|
|
38
|
+
* Without it every host reimplements it — jbrowse-anywidget carried a
|
|
39
|
+
* line-for-line Python translation of core's hubUrl/fetchHub/addRelativeUris,
|
|
40
|
+
* GenArk regex and all, purely because the multi-view product could not take a
|
|
41
|
+
* hub name.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveAssemblies(inputs: AssemblyInput[]): Promise<ResolvedAssemblies>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fetchHub } from '@jbrowse/core/util/fetchHub';
|
|
2
|
+
import { isSequenceUri, makeAssembly } from '@jbrowse/core/util/makeAssembly';
|
|
3
|
+
// A hub's config arrives off the network, so its search adapters are untyped
|
|
4
|
+
// there. Keep the ones that carry the id everything downstream references,
|
|
5
|
+
// rather than casting the lot: an entry without one cannot be referred to by a
|
|
6
|
+
// config anyway, so dropping it loses nothing and keeps this cast-free.
|
|
7
|
+
function searchAdaptersOf(hub) {
|
|
8
|
+
const found = (hub.aggregateTextSearchAdapters ?? []).filter((a) => typeof a.textSearchAdapterId === 'string');
|
|
9
|
+
return found.length ? found : undefined;
|
|
10
|
+
}
|
|
11
|
+
function fromHubConfig(hub) {
|
|
12
|
+
const assembly = hub.assemblies?.[0];
|
|
13
|
+
if (!assembly) {
|
|
14
|
+
throw new Error('hub config has no assemblies');
|
|
15
|
+
}
|
|
16
|
+
return { assembly, aggregateTextSearchAdapters: searchAdaptersOf(hub) };
|
|
17
|
+
}
|
|
18
|
+
/** One assembly in any of the four accepted shapes. */
|
|
19
|
+
export async function resolveAssembly(input) {
|
|
20
|
+
if (typeof input === 'string') {
|
|
21
|
+
return isSequenceUri(input)
|
|
22
|
+
? { assembly: makeAssembly({ fastaUri: input }) }
|
|
23
|
+
: fromHubConfig(await fetchHub(input));
|
|
24
|
+
}
|
|
25
|
+
else if ('assemblies' in input) {
|
|
26
|
+
return fromHubConfig(input);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return { assembly: input };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a whole list of assembly inputs, so a multi-assembly product takes
|
|
34
|
+
* the same vocabulary the single-view one does — `['hg38', 'mm39']` as readily
|
|
35
|
+
* as two hand-written configs.
|
|
36
|
+
*
|
|
37
|
+
* Exported because the resolution is async and `createApp` is not: an engine
|
|
38
|
+
* that resolved its own genomes would have to hand back a controller whose
|
|
39
|
+
* `viewState` is undefined until the fetches land, which is most of what makes
|
|
40
|
+
* the single-view controller four times the size. A host builds its options
|
|
41
|
+
* asynchronously already (runtime plugins), so this belongs in that step.
|
|
42
|
+
*
|
|
43
|
+
* Without it every host reimplements it — jbrowse-anywidget carried a
|
|
44
|
+
* line-for-line Python translation of core's hubUrl/fetchHub/addRelativeUris,
|
|
45
|
+
* GenArk regex and all, purely because the multi-view product could not take a
|
|
46
|
+
* hub name.
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveAssemblies(inputs) {
|
|
49
|
+
const resolved = await Promise.all(inputs.map(resolveAssembly));
|
|
50
|
+
const adapters = resolved.flatMap(r => r.aggregateTextSearchAdapters ?? []);
|
|
51
|
+
return {
|
|
52
|
+
assemblies: resolved.map(r => r.assembly),
|
|
53
|
+
aggregateTextSearchAdapters: adapters.length ? adapters : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|