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