@jbrowse/product-core 4.2.1 → 5.0.0-beta.1

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 (133) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +629 -304
  2. package/esm/RootModel/BaseRootModel.js +138 -14
  3. package/esm/RootModel/HierarchicalConfig.d.ts +57 -21
  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 +53 -0
  8. package/esm/RootModel/PreferencesConfig.js +61 -0
  9. package/esm/RootModel/createConfigModel.d.ts +232 -0
  10. package/esm/RootModel/createConfigModel.js +56 -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 +143 -0
  15. package/esm/Session/BaseSession.d.ts +310 -11
  16. package/esm/Session/BaseSession.js +448 -14
  17. package/esm/Session/Connections.d.ts +114 -8
  18. package/esm/Session/Connections.js +195 -74
  19. package/esm/Session/DrawerWidgets.d.ts +115 -12
  20. package/esm/Session/DrawerWidgets.js +161 -42
  21. package/esm/Session/MultipleViews.d.ts +1266 -1128
  22. package/esm/Session/MultipleViews.js +357 -66
  23. package/esm/Session/Preferences.d.ts +1665 -0
  24. package/esm/Session/Preferences.js +54 -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 +1275 -1138
  28. package/esm/Session/SessionTracks.js +532 -35
  29. package/esm/Session/Themes.d.ts +87 -2
  30. package/esm/Session/Themes.js +156 -23
  31. package/esm/Session/TrackMenu.d.ts +112 -0
  32. package/esm/Session/TrackMenu.js +200 -0
  33. package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
  34. package/esm/Session/TrackMenuSessionMixin.js +41 -0
  35. package/esm/Session/Tracks.d.ts +1169 -1117
  36. package/esm/Session/Tracks.js +160 -20
  37. package/esm/Session/assertSessionModel.d.ts +21 -0
  38. package/esm/Session/assertSessionModel.js +1 -0
  39. package/esm/Session/finalizeSession.d.ts +16 -0
  40. package/esm/Session/finalizeSession.js +29 -0
  41. package/esm/Session/index.d.ts +22 -10
  42. package/esm/Session/index.js +13 -10
  43. package/esm/Session/shareableSnapshot.d.ts +77 -0
  44. package/esm/Session/shareableSnapshot.js +196 -0
  45. package/esm/Session/temporaryAssemblyTracks.d.ts +23 -0
  46. package/esm/Session/temporaryAssemblyTracks.js +38 -0
  47. package/esm/assertExtends.d.ts +55 -0
  48. package/esm/assertExtends.js +1 -0
  49. package/esm/controllerTracks.d.ts +56 -0
  50. package/esm/controllerTracks.js +77 -0
  51. package/esm/destroyViewState.d.ts +21 -0
  52. package/esm/destroyViewState.js +29 -0
  53. package/esm/index.d.ts +31 -4
  54. package/esm/index.js +31 -4
  55. package/esm/loadPlugins.d.ts +33 -0
  56. package/esm/loadPlugins.js +38 -0
  57. package/esm/localFiles.d.ts +74 -0
  58. package/esm/localFiles.js +156 -0
  59. package/esm/observeSession.d.ts +56 -0
  60. package/esm/observeSession.js +91 -0
  61. package/esm/pluginInput.d.ts +32 -0
  62. package/esm/pluginInput.js +19 -0
  63. package/esm/pruneUnbuildableNodes.d.ts +109 -0
  64. package/esm/pruneUnbuildableNodes.js +568 -0
  65. package/esm/resolveAssemblies.d.ts +44 -0
  66. package/esm/resolveAssemblies.js +55 -0
  67. package/esm/rpcWorker.d.ts +26 -0
  68. package/esm/rpcWorker.js +96 -15
  69. package/esm/scheduleDetachedDestroy.d.ts +18 -0
  70. package/esm/scheduleDetachedDestroy.js +24 -0
  71. package/esm/sessionMigrations/index.d.ts +53 -0
  72. package/esm/sessionMigrations/index.js +432 -0
  73. package/esm/sessionUrl.d.ts +41 -0
  74. package/esm/sessionUrl.js +53 -0
  75. package/esm/sessionUtils.d.ts +69 -0
  76. package/esm/sessionUtils.js +613 -0
  77. package/esm/siblingCast.d.ts +14 -0
  78. package/esm/siblingCast.js +16 -0
  79. package/esm/snapshotUtils.d.ts +8 -0
  80. package/esm/snapshotUtils.js +12 -0
  81. package/esm/ui/AboutDialog.d.ts +3 -6
  82. package/esm/ui/AboutDialog.js +4 -4
  83. package/esm/ui/AboutDialogContents.d.ts +2 -6
  84. package/esm/ui/AboutDialogContents.js +34 -13
  85. package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
  86. package/esm/ui/AssemblyInfoPanel.js +69 -0
  87. package/esm/ui/DisplayDefaultsSection.d.ts +36 -0
  88. package/esm/ui/DisplayDefaultsSection.js +65 -0
  89. package/esm/ui/Drawer.d.ts +21 -0
  90. package/esm/ui/Drawer.js +60 -0
  91. package/esm/ui/DrawerControls.d.ts +5 -0
  92. package/esm/ui/DrawerControls.js +28 -0
  93. package/esm/ui/DrawerHeader.d.ts +6 -0
  94. package/esm/ui/DrawerHeader.js +36 -0
  95. package/esm/ui/DrawerWidget.d.ts +5 -0
  96. package/esm/ui/DrawerWidget.js +12 -0
  97. package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
  98. package/esm/ui/DrawerWidgetSelector.js +40 -0
  99. package/esm/ui/FileInfoPanel.d.ts +3 -6
  100. package/esm/ui/FileInfoPanel.js +35 -26
  101. package/esm/ui/HeaderButtons.d.ts +7 -1
  102. package/esm/ui/HeaderButtons.js +23 -18
  103. package/esm/ui/ModalWidget.d.ts +12 -0
  104. package/esm/ui/ModalWidget.js +33 -0
  105. package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
  106. package/esm/ui/ModalWidgetAppBar.js +9 -0
  107. package/esm/ui/PreferencesDialog.d.ts +56 -0
  108. package/esm/ui/PreferencesDialog.js +145 -0
  109. package/esm/ui/PreferencesResetDialog.d.ts +8 -0
  110. package/esm/ui/PreferencesResetDialog.js +19 -0
  111. package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
  112. package/esm/ui/RefNameAliasesDialog.js +65 -0
  113. package/esm/ui/RefNameInfoDialog.d.ts +3 -6
  114. package/esm/ui/RefNameInfoDialog.js +57 -63
  115. package/esm/ui/WidgetBody.d.ts +18 -0
  116. package/esm/ui/WidgetBody.js +22 -0
  117. package/esm/ui/WidgetHeading.d.ts +7 -0
  118. package/esm/ui/WidgetHeading.js +7 -0
  119. package/esm/ui/drawerLayout.d.ts +18 -0
  120. package/esm/ui/drawerLayout.js +22 -0
  121. package/esm/ui/index.d.ts +13 -1
  122. package/esm/ui/index.js +12 -1
  123. package/esm/ui/util.d.ts +37 -4
  124. package/esm/ui/util.js +35 -38
  125. package/esm/useEngineLifecycle.d.ts +13 -0
  126. package/esm/useEngineLifecycle.js +22 -0
  127. package/package.json +11 -15
  128. package/esm/RootModel/FormatAbout.d.ts +0 -12
  129. package/esm/RootModel/FormatAbout.js +0 -15
  130. package/esm/RootModel/FormatDetails.d.ts +0 -24
  131. package/esm/RootModel/FormatDetails.js +0 -27
  132. package/esm/Session/DialogQueue.d.ts +0 -17
  133. 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
+ }