@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.
Files changed (157) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +514 -323
  2. package/esm/RootModel/BaseRootModel.js +141 -14
  3. package/esm/RootModel/HierarchicalConfig.d.ts +30 -20
  4. package/esm/RootModel/HierarchicalConfig.js +36 -0
  5. package/esm/RootModel/InternetAccounts.d.ts +18 -1
  6. package/esm/RootModel/InternetAccounts.js +120 -22
  7. package/esm/RootModel/PreferencesConfig.d.ts +37 -0
  8. package/esm/RootModel/PreferencesConfig.js +75 -0
  9. package/esm/RootModel/createConfigModel.d.ts +257 -0
  10. package/esm/RootModel/createConfigModel.js +77 -0
  11. package/esm/RootModel/index.d.ts +10 -5
  12. package/esm/RootModel/index.js +10 -5
  13. package/esm/RootModel/menuItems.d.ts +24 -0
  14. package/esm/RootModel/menuItems.js +141 -0
  15. package/esm/Session/BaseSession.d.ts +285 -11
  16. package/esm/Session/BaseSession.js +383 -15
  17. package/esm/Session/Connections.d.ts +115 -8
  18. package/esm/Session/Connections.js +196 -74
  19. package/esm/Session/DrawerWidgets.d.ts +145 -13
  20. package/esm/Session/DrawerWidgets.js +219 -49
  21. package/esm/Session/MultipleViews.d.ts +1067 -1177
  22. package/esm/Session/MultipleViews.js +395 -64
  23. package/esm/Session/Preferences.d.ts +1389 -0
  24. package/esm/Session/Preferences.js +58 -0
  25. package/esm/Session/ReferenceManagement.d.ts +28 -3
  26. package/esm/Session/ReferenceManagement.js +56 -41
  27. package/esm/Session/SessionTracks.d.ts +1101 -1180
  28. package/esm/Session/SessionTracks.js +584 -34
  29. package/esm/Session/Themes.d.ts +136 -2
  30. package/esm/Session/Themes.js +267 -21
  31. package/esm/Session/TrackMenu.d.ts +88 -0
  32. package/esm/Session/TrackMenu.js +175 -0
  33. package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
  34. package/esm/Session/TrackMenuSessionMixin.js +42 -0
  35. package/esm/Session/Tracks.d.ts +945 -1159
  36. package/esm/Session/Tracks.js +229 -30
  37. package/esm/Session/assertSessionModel.d.ts +21 -0
  38. package/esm/Session/assertSessionModel.js +1 -0
  39. package/esm/Session/copyTrackSnapshot.d.ts +21 -0
  40. package/esm/Session/copyTrackSnapshot.js +20 -0
  41. package/esm/Session/developerMode.d.ts +7 -0
  42. package/esm/Session/developerMode.js +43 -0
  43. package/esm/Session/finalizeSession.d.ts +15 -0
  44. package/esm/Session/finalizeSession.js +28 -0
  45. package/esm/Session/hydratedForms.d.ts +16 -0
  46. package/esm/Session/hydratedForms.js +13 -0
  47. package/esm/Session/index.d.ts +27 -10
  48. package/esm/Session/index.js +16 -10
  49. package/esm/Session/pluginExtraTrackItems.d.ts +11 -0
  50. package/esm/Session/pluginExtraTrackItems.js +13 -0
  51. package/esm/Session/readdedTrackConf.d.ts +8 -0
  52. package/esm/Session/readdedTrackConf.js +24 -0
  53. package/esm/Session/shareableSnapshot.d.ts +23 -0
  54. package/esm/Session/shareableSnapshot.js +66 -0
  55. package/esm/Session/temporaryAssemblyTracks.d.ts +24 -0
  56. package/esm/Session/temporaryAssemblyTracks.js +37 -0
  57. package/esm/assertExtends.d.ts +55 -0
  58. package/esm/assertExtends.js +1 -0
  59. package/esm/controllerTracks.d.ts +76 -0
  60. package/esm/controllerTracks.js +125 -0
  61. package/esm/destroyViewState.d.ts +21 -0
  62. package/esm/destroyViewState.js +29 -0
  63. package/esm/embeddedController.d.ts +57 -0
  64. package/esm/embeddedController.js +100 -0
  65. package/esm/index.d.ts +32 -4
  66. package/esm/index.js +31 -4
  67. package/esm/loadPlugins.d.ts +38 -0
  68. package/esm/loadPlugins.js +45 -0
  69. package/esm/localFiles.d.ts +74 -0
  70. package/esm/localFiles.js +156 -0
  71. package/esm/observeSession.d.ts +55 -0
  72. package/esm/observeSession.js +90 -0
  73. package/esm/parallelChunkLoading.d.ts +30 -0
  74. package/esm/parallelChunkLoading.js +71 -0
  75. package/esm/pluginInput.d.ts +32 -0
  76. package/esm/pluginInput.js +19 -0
  77. package/esm/pruneUnbuildableNodes.d.ts +109 -0
  78. package/esm/pruneUnbuildableNodes.js +562 -0
  79. package/esm/resolveAssemblies.d.ts +68 -0
  80. package/esm/resolveAssemblies.js +77 -0
  81. package/esm/rpcWorker.d.ts +36 -4
  82. package/esm/rpcWorker.js +126 -20
  83. package/esm/sessionMigrations/index.d.ts +15 -0
  84. package/esm/sessionMigrations/index.js +178 -0
  85. package/esm/sessionUrl.d.ts +40 -0
  86. package/esm/sessionUrl.js +66 -0
  87. package/esm/sessionUtils.d.ts +74 -0
  88. package/esm/sessionUtils.js +599 -0
  89. package/esm/siblingCast.d.ts +14 -0
  90. package/esm/siblingCast.js +16 -0
  91. package/esm/snapshotUtils.d.ts +8 -0
  92. package/esm/snapshotUtils.js +12 -0
  93. package/esm/ui/AboutDialog.d.ts +8 -3
  94. package/esm/ui/AboutDialog.js +14 -5
  95. package/esm/ui/AboutDialogContents.d.ts +2 -6
  96. package/esm/ui/AboutDialogContents.js +13 -17
  97. package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
  98. package/esm/ui/AssemblyInfoPanel.js +63 -0
  99. package/esm/ui/DescriptionPanel.d.ts +3 -0
  100. package/esm/ui/DescriptionPanel.js +25 -0
  101. package/esm/ui/Drawer.d.ts +21 -0
  102. package/esm/ui/Drawer.js +60 -0
  103. package/esm/ui/DrawerControls.d.ts +5 -0
  104. package/esm/ui/DrawerControls.js +28 -0
  105. package/esm/ui/DrawerHeader.d.ts +6 -0
  106. package/esm/ui/DrawerHeader.js +36 -0
  107. package/esm/ui/DrawerWidget.d.ts +5 -0
  108. package/esm/ui/DrawerWidget.js +12 -0
  109. package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
  110. package/esm/ui/DrawerWidgetSelector.js +40 -0
  111. package/esm/ui/FileInfoPanel.d.ts +3 -6
  112. package/esm/ui/FileInfoPanel.js +36 -26
  113. package/esm/ui/GeneralPreferencesTab.d.ts +17 -0
  114. package/esm/ui/GeneralPreferencesTab.js +34 -0
  115. package/esm/ui/HeaderButtons.d.ts +2 -1
  116. package/esm/ui/HeaderButtons.js +15 -17
  117. package/esm/ui/ModalWidget.d.ts +13 -0
  118. package/esm/ui/ModalWidget.js +48 -0
  119. package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
  120. package/esm/ui/ModalWidgetAppBar.js +9 -0
  121. package/esm/ui/PreferenceCheckbox.d.ts +6 -0
  122. package/esm/ui/PreferenceCheckbox.js +16 -0
  123. package/esm/ui/PreferencesDialog.d.ts +34 -0
  124. package/esm/ui/PreferencesDialog.js +55 -0
  125. package/esm/ui/PreferencesResetDialog.d.ts +8 -0
  126. package/esm/ui/PreferencesResetDialog.js +19 -0
  127. package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
  128. package/esm/ui/RefNameAliasesDialog.js +65 -0
  129. package/esm/ui/RefNameInfoDialog.d.ts +3 -6
  130. package/esm/ui/RefNameInfoDialog.js +45 -63
  131. package/esm/ui/ViewPreferencesTab.d.ts +12 -0
  132. package/esm/ui/ViewPreferencesTab.js +15 -0
  133. package/esm/ui/WidgetBody.d.ts +18 -0
  134. package/esm/ui/WidgetBody.js +22 -0
  135. package/esm/ui/WidgetHeading.d.ts +7 -0
  136. package/esm/ui/WidgetHeading.js +7 -0
  137. package/esm/ui/aboutTestUtils.d.ts +140 -0
  138. package/esm/ui/aboutTestUtils.js +25 -0
  139. package/esm/ui/descriptionHtml.d.ts +25 -0
  140. package/esm/ui/descriptionHtml.js +64 -0
  141. package/esm/ui/drawerLayout.d.ts +18 -0
  142. package/esm/ui/drawerLayout.js +22 -0
  143. package/esm/ui/index.d.ts +15 -1
  144. package/esm/ui/index.js +13 -1
  145. package/esm/ui/preferencesReset.d.ts +18 -0
  146. package/esm/ui/preferencesReset.js +69 -0
  147. package/esm/ui/util.d.ts +22 -7
  148. package/esm/ui/util.js +19 -41
  149. package/esm/useEngineLifecycle.d.ts +19 -0
  150. package/esm/useEngineLifecycle.js +44 -0
  151. package/package.json +15 -18
  152. package/esm/RootModel/FormatAbout.d.ts +0 -12
  153. package/esm/RootModel/FormatAbout.js +0 -15
  154. package/esm/RootModel/FormatDetails.d.ts +0 -24
  155. package/esm/RootModel/FormatDetails.js +0 -27
  156. package/esm/Session/DialogQueue.d.ts +0 -17
  157. 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
+ }