@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,599 @@
1
+ import { maybePluginUrl } from '@jbrowse/core/pluginDefinitions';
2
+ import { diffTrackConfig, flattenTrackConfigDelta } from '@jbrowse/core/util';
3
+ import { getChildType, getPropertyMembers, getSnapshot, isArrayType, isMapType, isModelType, isReferenceType, isValidReference, } from '@jbrowse/mobx-state-tree';
4
+ import { asArray, isRecord } from './snapshotUtils.js';
5
+ // Cleans a freshly-loaded session in place by dropping any array/map element
6
+ // that can't be hydrated — e.g. an open track whose `configuration` reference
7
+ // resolves to a dangling id or a structurally-invalid config and throws when
8
+ // read. Dropping keeps the invariant that the open set only ever holds usable
9
+ // tracks (matching the open/add paths, which refuse invalid configs), so
10
+ // downstream code never has to defend against a track whose config access
11
+ // throws. Returns the dropped nodes so the caller can tell the user which
12
+ // tracks went missing.
13
+ //
14
+ // Dangling references in collections are NOT this function's job: every session
15
+ // reference is a `types.safeReference`, whose onInvalidated removes the entry
16
+ // from its parent at load. The `isValidReference` branch below is a backstop
17
+ // for any future plain `types.reference` collection.
18
+ export function filterSessionInPlace(node, type, dropped = []) {
19
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
20
+ if (node !== undefined) {
21
+ if (isArrayType(type)) {
22
+ const array = node;
23
+ const childType = getChildType(node);
24
+ const isRef = isReferenceType(childType);
25
+ for (let i = 0; i < array.length;) {
26
+ if (!walkChildOrDrop(() => array[i], childType, isRef, dropped)) {
27
+ array.splice(i, 1);
28
+ }
29
+ else {
30
+ i += 1;
31
+ }
32
+ }
33
+ }
34
+ else if (isMapType(type)) {
35
+ const map = node;
36
+ const childType = getChildType(map);
37
+ const isRef = isReferenceType(childType);
38
+ for (const key of map.keys()) {
39
+ if (!walkChildOrDrop(() => map.get(key), childType, isRef, dropped)) {
40
+ map.delete(key);
41
+ }
42
+ }
43
+ }
44
+ else if (isModelType(type)) {
45
+ const { properties } = getPropertyMembers(node);
46
+ // A node holding a `configuration` reference is a track/display/
47
+ // connection. Its only load-time integrity concern is that the config
48
+ // resolves: reading it throws for a dangling id or a structurally-invalid
49
+ // config, which the caller turns into a drop. Stop here instead of
50
+ // recursing — the first config-bearing node on the path to a display is
51
+ // the track itself, and recursing past it would eagerly instantiate child
52
+ // state models (displays) and run their afterAttach before the view is
53
+ // measured. That throws on view.width and would be misread as an invalid
54
+ // node, dropping a valid track. Display configs resolve through their own
55
+ // safety net (the track config's preProcessSnapshot injects a stub
56
+ // display per registered type).
57
+ if ('configuration' in properties) {
58
+ void node.configuration;
59
+ }
60
+ else {
61
+ for (const [pname, ptype] of Object.entries(properties)) {
62
+ filterSessionInPlace(node[pname], ptype, dropped);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ return dropped;
68
+ }
69
+ // Returns false if the collection element should be dropped: a dangling
70
+ // reference (removed silently — see header), or a config-bearing node that
71
+ // throws while hydrating (recorded in `dropped` so the caller can report it).
72
+ // Otherwise recurses into it and returns true.
73
+ function walkChildOrDrop(get, childType, isRef, dropped) {
74
+ if (isRef) {
75
+ return isValidReference(get);
76
+ }
77
+ let child;
78
+ try {
79
+ child = get();
80
+ filterSessionInPlace(child, childType, dropped);
81
+ return true;
82
+ }
83
+ catch (e) {
84
+ console.error(e);
85
+ dropped.push(describeDroppedNode(child));
86
+ return false;
87
+ }
88
+ }
89
+ // Identifies a dropped node for a user-facing message by reading its serialized
90
+ // snapshot, which keeps a dangling `configuration` reference (the usual cause of
91
+ // a drop) from throwing again — references serialize to their stored id string.
92
+ function describeDroppedNode(node) {
93
+ try {
94
+ if (node !== undefined) {
95
+ const snap = getSnapshot(node);
96
+ return {
97
+ type: typeof snap.type === 'string' ? snap.type : undefined,
98
+ configuration: typeof snap.configuration === 'string'
99
+ ? snap.configuration
100
+ : undefined,
101
+ };
102
+ }
103
+ }
104
+ catch (e) {
105
+ console.error(e);
106
+ }
107
+ return {};
108
+ }
109
+ function locationDisplayName(loc) {
110
+ switch (loc.locationType) {
111
+ case 'LocalPathLocation':
112
+ return typeof loc.localPath === 'string' ? loc.localPath : 'local file';
113
+ case 'BlobLocation':
114
+ case 'FileHandleLocation':
115
+ return typeof loc.name === 'string' ? loc.name : 'local file';
116
+ case 'UriLocation':
117
+ return typeof loc.uri === 'string' ? loc.uri : 'remote file';
118
+ default:
119
+ return 'unknown location';
120
+ }
121
+ }
122
+ function walkLocations(node, track, out) {
123
+ if (Array.isArray(node)) {
124
+ for (const item of node) {
125
+ walkLocations(item, track, out);
126
+ }
127
+ }
128
+ else if (typeof node === 'object' && node !== null) {
129
+ const obj = node;
130
+ const nextTrack = typeof obj.trackId === 'string'
131
+ ? {
132
+ trackId: obj.trackId,
133
+ trackName: typeof obj.name === 'string' ? obj.name : undefined,
134
+ }
135
+ : track;
136
+ if (typeof obj.locationType === 'string' &&
137
+ obj.locationType !== 'UriLocation') {
138
+ out.push({
139
+ locationType: obj.locationType,
140
+ name: locationDisplayName(obj),
141
+ trackId: nextTrack?.trackId,
142
+ trackName: nextTrack?.trackName,
143
+ });
144
+ }
145
+ for (const key of Object.keys(obj)) {
146
+ walkLocations(obj[key], nextTrack, out);
147
+ }
148
+ }
149
+ }
150
+ // Walks a session/config snapshot and reports every file location that won't
151
+ // open on jbrowse-web. Anything that isn't a UriLocation (local paths from
152
+ // desktop, or blob/file-handle locations bound to another browser session) is
153
+ // non-portable; a fully-UriLocation snapshot is portable and can be handed to
154
+ // the web app directly.
155
+ export function analyzeWebPortability(snapshot) {
156
+ const nonPortable = [];
157
+ walkLocations(snapshot, undefined, nonPortable);
158
+ return { nonPortable, portable: nonPortable.length === 0 };
159
+ }
160
+ // Distinct file display names of a set of non-portable locations, in first-seen
161
+ // order.
162
+ function distinctNames(locations) {
163
+ return [...new Set(locations.map(l => l.name))];
164
+ }
165
+ // A string-valued field off a loosely-typed snapshot entry, or undefined
166
+ function readString(entry, key) {
167
+ const value = isRecord(entry) ? entry[key] : undefined;
168
+ return typeof value === 'string' ? value : undefined;
169
+ }
170
+ function readTrackId(track) {
171
+ return readString(track, 'trackId');
172
+ }
173
+ function readAssemblyName(assembly) {
174
+ return readString(assembly, 'name');
175
+ }
176
+ // Concatenate snapshot lists, keeping the last entry per `key` so one carried by
177
+ // both the prior session and the current snapshot ships once. Entries without a
178
+ // readable key are kept as-is, in order.
179
+ //
180
+ // A duplicate trackId is an MST identifier collision on load. A duplicate
181
+ // assembly name is worse: it doesn't fail at load, it makes every assembly's
182
+ // `configuration` safeReference ambiguous, so the assemblyManager throws on the
183
+ // next read of one and the session is dead.
184
+ function concatLastByKey(key, ...lists) {
185
+ const byKey = new Map();
186
+ const out = [];
187
+ for (const entry of lists.flat()) {
188
+ const id = readString(entry, key);
189
+ if (id === undefined) {
190
+ out.push(entry);
191
+ }
192
+ else {
193
+ byKey.set(id, entry);
194
+ }
195
+ }
196
+ return [...out, ...byKey.values()];
197
+ }
198
+ // Session assemblies that don't collide with one the recipient's config already
199
+ // provides. `sessionAssemblies` and `jbrowse.assemblies` are concatenated into
200
+ // one namespace on the far side (`AssembliesMixin.assemblies`), and the
201
+ // duplicate-name guard there lives in `addSessionAssembly` — an *add* path,
202
+ // which a deserialized snapshot never goes through. So a name the hosted base
203
+ // also uses is not rejected on load, it silently makes every assembly's
204
+ // `configuration` safeReference ambiguous and takes the session down on the
205
+ // next read. The base's copy wins because it is the one `?config=` guarantees
206
+ // is there.
207
+ //
208
+ // Only the hostedConfigBase strategy needs this. A self-contained export ships
209
+ // no config at all, and concatLastByKey already keeps its own two lists
210
+ // from colliding with each other.
211
+ function withoutBaseAssemblies(sessionAssemblies, baseAssemblyNames) {
212
+ return sessionAssemblies.filter(a => {
213
+ const name = readAssemblyName(a);
214
+ return !name || !baseAssemblyNames.has(name);
215
+ });
216
+ }
217
+ // The assemblies a hosted-base export silently hands back to the hosted config.
218
+ // `coveredByBase` matches on name alone, and nothing ships an assembly edit: a
219
+ // user who attached a refNameAlias or cytoband file to a hub assembly exports a
220
+ // session that quietly uses the hub's original. Both halves of the loss are the
221
+ // same fact to a sender, so they are reported as one list — a config assembly
222
+ // whose config differs from the base's, and a session assembly that
223
+ // `withoutBaseAssemblies` drops to keep the recipient's safeReferences
224
+ // unambiguous.
225
+ //
226
+ // Edited-ness is `editedDelta`, the same gate `splitTracksAgainstBase` uses, so
227
+ // an export can't call an assembly edited on a diff too thin to be worth
228
+ // shipping for a track.
229
+ function assembliesRevertedToBase(assemblies, sessionAssemblies, baseAssemblies, form) {
230
+ const baseByName = new Map(baseAssemblies.map(a => [a.name, a]));
231
+ const edited = assemblies.flatMap(assembly => {
232
+ const base = baseByName.get(assembly.name);
233
+ return base && editedDelta(base, assembly, form) ? [assembly.name] : [];
234
+ });
235
+ const shadowed = sessionAssemblies.flatMap(a => {
236
+ const name = readAssemblyName(a);
237
+ return name && baseByName.has(name) ? [name] : [];
238
+ });
239
+ return [...new Set([...edited, ...shadowed])];
240
+ }
241
+ // The `sessionAssemblies` override for a hosted-base session, or nothing at all
242
+ // when the session had none — the spread of `defaultSession` is what carries
243
+ // them otherwise, so an unconditional key would add an empty array to every
244
+ // export that never had one.
245
+ function hostedSessionAssemblies(prior, baseAssemblyNames) {
246
+ return prior.length > 0
247
+ ? { sessionAssemblies: withoutBaseAssemblies(prior, baseAssemblyNames) }
248
+ : {};
249
+ }
250
+ // The trackIds the export is free to drop: the ones that name an entry in a
251
+ // track list it actually assembles. Everything else a location can be tagged
252
+ // with is structural and stays — an assembly's own sequence config carries
253
+ // `<name>-ReferenceSequenceTrack`, and an open connection track's config lives
254
+ // in `connectionTrackConfigs`, which rides along inside the session snapshot.
255
+ function droppableTrackIds(tracks, sessionTracks) {
256
+ return new Set([...tracks, ...sessionTracks].flatMap(t => readTrackId(t) ?? []));
257
+ }
258
+ // Drops a track's `textSearching` when its index is local — the desktop indexer
259
+ // writes the Trix ix/ixx/meta triple as LocalPathLocations
260
+ // (`addTrackTextSearchConf`), so indexing a remote BAM on desktop is enough to
261
+ // make an otherwise perfectly portable track non-portable and get it dropped
262
+ // whole. The data file is the track; the index is a search box over it. Shedding
263
+ // the index costs the recipient that box and nothing else.
264
+ //
265
+ // A hub track takes the hub's own index in its place, so its delta records no
266
+ // change there and the recipient searches with the hub's index. Leaving the
267
+ // member out would be a reset of it.
268
+ function withoutLocalTextSearch(track, hubIndex) {
269
+ if (!isRecord(track) ||
270
+ !isRecord(track.textSearching) ||
271
+ analyzeWebPortability(track.textSearching).portable) {
272
+ return track;
273
+ }
274
+ const { textSearching, ...rest } = track;
275
+ return (hubIndex === undefined ? rest : { ...rest, textSearching: hubIndex });
276
+ }
277
+ // Display names of the tracks `withoutLocalTextSearch` stripped, for the report.
278
+ function strippedTextIndexNames(before, after) {
279
+ return [
280
+ ...new Set(before.flatMap((track, i) => track !== after[i] && isRecord(track)
281
+ ? [
282
+ typeof track.name === 'string'
283
+ ? track.name
284
+ : (readTrackId(track) ?? 'unknown track'),
285
+ ]
286
+ : [])),
287
+ ];
288
+ }
289
+ // The tracks that reference a local file and can be shed by dropping them:
290
+ // their trackIds, to filter the shipped track lists with, and display names, to
291
+ // report to the user. A local file that no droppable track owns can't be shed
292
+ // this way and surfaces as a blockingFile instead.
293
+ function nonPortableUserTracks(report, droppable) {
294
+ const locs = report.nonPortable.flatMap(l => l.trackId && droppable.has(l.trackId)
295
+ ? [{ trackId: l.trackId, name: l.trackName ?? l.trackId }]
296
+ : []);
297
+ return {
298
+ ids: new Set(locs.map(l => l.trackId)),
299
+ names: [...new Set(locs.map(l => l.name))],
300
+ };
301
+ }
302
+ // Splits the portable tracks against the hosted base. A track with no base entry
303
+ // is user-added and ships whole in `addedTracks` (→ sessionTracks). A track that
304
+ // matches a base entry but differs from it becomes an `editDeltas` entry — the
305
+ // same channel the web session uses — so the recipient's base is overlaid with
306
+ // what the sender has. A track equal to its base entry produces neither and
307
+ // resolves from the base.
308
+ //
309
+ // "Differs" is not the same claim as "the user edited it", and the difference is
310
+ // worth naming. The oracle here is the hub as it is NOW, while desktop's copy is
311
+ // the hub as it was when the session was created (desktop edits jbrowse.tracks in
312
+ // place, keeping the base trackId, and persists the whole config). So a track the
313
+ // hub itself changed since then also lands in `editDeltas`, pinning the recipient
314
+ // to the values the sender is looking at. That is the export's stated intent —
315
+ // reproduce the sender's screen, the same reason `bakeSessionCascades` exists —
316
+ // but the delta channel it travels in means "user override" on the far side, and
317
+ // a delta masks later admin changes to that track. Telling the two apart needs
318
+ // the base as it was at session creation, which nothing persists.
319
+ function splitTracksAgainstBase(tracks, baseTracks, form) {
320
+ const baseById = new Map(baseTracks.map(t => [t.trackId, t]));
321
+ const addedTracks = tracks.filter(t => !baseById.has(t.trackId));
322
+ const editDeltas = Object.fromEntries(tracks.flatMap((track) => {
323
+ const base = baseById.get(track.trackId);
324
+ const delta = base && editedDelta(base, track, form);
325
+ return delta ? [[track.trackId, delta]] : [];
326
+ }));
327
+ return { addedTracks, editDeltas };
328
+ }
329
+ // The delta from `base` to `edited` with both in their hydrated form, or
330
+ // undefined when no setting changed — the web session's own "is edited" test,
331
+ // which a bare key count fails on the identity keys and display stubs
332
+ function editedDelta(base, edited, form) {
333
+ const hydratedBase = form(base);
334
+ const delta = diffTrackConfig(hydratedBase, form(edited));
335
+ return flattenTrackConfigDelta(hydratedBase, delta).length > 0
336
+ ? delta
337
+ : undefined;
338
+ }
339
+ // Overlays edited-track deltas onto the session's trackConfigDeltas, preserving
340
+ // any the base session already carried. A no-op (returns the session unchanged)
341
+ // when there are no edits, so the exported snapshot stays minimal.
342
+ function withDeltas(session, editDeltas) {
343
+ if (Object.keys(editDeltas).length === 0) {
344
+ return session;
345
+ }
346
+ const prior = isRecord(session.trackConfigDeltas)
347
+ ? session.trackConfigDeltas
348
+ : {};
349
+ return { ...session, trackConfigDeltas: { ...prior, ...editDeltas } };
350
+ }
351
+ // The plugin definitions the recipient will not already have, carried in the
352
+ // session as `sessionPlugins` — the one channel a jbrowse-web session has for
353
+ // them, and one it triages before loading (SessionLoader.loadSession). Without
354
+ // this, a desktop session using a plugin's view or track type opens on a
355
+ // jbrowse-web that never registered the type, and the view is dropped on load.
356
+ //
357
+ // Under `hostedConfigBase` jbrowse-web loads the base config's own `plugins[]`,
358
+ // so only the extras ride along; a `selfContained` export opens `?config=none`
359
+ // and has no config to load any from, so it carries them all. Identity is the
360
+ // url a definition loads from (`maybePluginUrl`) rather than its name, since an
361
+ // ESM/CJS definition need not have one; a definition naming no loader at all is
362
+ // never "the same plugin" as another, so it is always carried.
363
+ function extraPlugins(plugins, basePlugins) {
364
+ const baseUrls = new Set(basePlugins.flatMap(p => maybePluginUrl(p) ?? []));
365
+ return plugins.filter(p => {
366
+ const url = maybePluginUrl(p);
367
+ return !url || !baseUrls.has(url);
368
+ });
369
+ }
370
+ // Connections the recipient will not already have, carried the same way and for
371
+ // the same reason as the plugins above: desktop keeps them in `jbrowse
372
+ // .connections`, which nothing in the export ships, so without this a session
373
+ // with a track hub arrives on the web with the hub gone. Only the connection
374
+ // tracks the sender had open survive on their own, through the session's
375
+ // `connectionTrackConfigs`; the hub itself — and every track in it the recipient
376
+ // might want to open next — needs `sessionConnections`, jbrowse-web's
377
+ // session-level counterpart of `jbrowse.connections`.
378
+ //
379
+ // Identity is `connectionId`, which is what the recipient's own
380
+ // `connections` getter concatenates on, so shipping one the base already
381
+ // declares would list the same hub twice.
382
+ function extraConnections(connections, baseConnections) {
383
+ const readId = (c) => readString(c, 'connectionId');
384
+ const baseIds = new Set(baseConnections.flatMap(c => readId(c) ?? []));
385
+ return connections.filter(c => {
386
+ const id = readId(c);
387
+ return !id || !baseIds.has(id);
388
+ });
389
+ }
390
+ // Collects every `internetAccountId` a snapshot's file locations name, in
391
+ // first-seen order.
392
+ function referencedAccountIds(node, out) {
393
+ if (Array.isArray(node)) {
394
+ for (const item of node) {
395
+ referencedAccountIds(item, out);
396
+ }
397
+ }
398
+ else if (isRecord(node)) {
399
+ if (typeof node.internetAccountId === 'string') {
400
+ out.add(node.internetAccountId);
401
+ }
402
+ for (const value of Object.values(node)) {
403
+ referencedAccountIds(value, out);
404
+ }
405
+ }
406
+ return out;
407
+ }
408
+ // The accounts a shipped file names that the recipient has no way to build.
409
+ // Internet accounts live in the root config alone — there is no
410
+ // `sessionInternetAccounts` for one to ride in — so a hosted-base export carries
411
+ // exactly the accounts the base config declares, and a self-contained export
412
+ // (`?config=none`) carries none at all. The recipient does not crash on a
413
+ // missing one (`findAppropriateInternetAccount` returns null rather than pushing
414
+ // an unknown type into the union), it fetches the file unauthenticated and
415
+ // reports the 401, which is worth the sender knowing before they send the link.
416
+ function unavailableAccountIds(session, baseAccounts) {
417
+ const available = new Set(baseAccounts.flatMap(a => a.internetAccountId ?? []));
418
+ return [...referencedAccountIds(session, new Set())].filter(id => !available.has(id));
419
+ }
420
+ // Adds the session-level carriers to the exported session, leaving each key out
421
+ // entirely when it would be empty so the exported snapshot stays minimal (same
422
+ // rule as withDeltas above).
423
+ function withCarriedConfigs(session, plugins, connections) {
424
+ return {
425
+ ...session,
426
+ ...(plugins.length > 0 ? { sessionPlugins: plugins } : {}),
427
+ ...(connections.length > 0 ? { sessionConnections: connections } : {}),
428
+ };
429
+ }
430
+ // Decides how to hand a desktop session to jbrowse-web. When the session was
431
+ // launched from a hosted hub config (sourceConfigUrl) that still covers all of
432
+ // its assemblies, that config is reused as the base and only user-added/edited
433
+ // tracks ride along; otherwise the session is made self-contained. `base` holds
434
+ // the fetched hub config (already rebased with addRelativeUris by the caller),
435
+ // used to tell hub tracks from user-added ones and to diff edited hub tracks.
436
+ export function planWebExport(snapshot, base) {
437
+ const sourceConfigUrl = snapshot.configuration?.sourceConfigUrl;
438
+ const assemblies = snapshot.assemblies ?? [];
439
+ const defaultSession = snapshot.defaultSession ?? {};
440
+ const priorSessionAssemblies = asArray(defaultSession.sessionAssemblies);
441
+ const inputTracks = snapshot.tracks ?? [];
442
+ const inputSessionTracks = asArray(defaultSession.sessionTracks);
443
+ const baseConfig = base?.config;
444
+ const baseAssemblyNames = new Set((baseConfig?.assemblies ?? []).map(a => a.name));
445
+ const coveredByBase = !!sourceConfigUrl &&
446
+ !!base &&
447
+ assemblies.every(a => baseAssemblyNames.has(a.name));
448
+ const hubIndexes = new Map((coveredByBase ? (base.config.tracks ?? []) : []).map(t => [
449
+ t.trackId,
450
+ t.textSearching,
451
+ ]));
452
+ const allTracks = inputTracks.map(t => withoutLocalTextSearch(t, hubIndexes.get(t.trackId)));
453
+ const allSessionTracks = inputSessionTracks.map(t => withoutLocalTextSearch(t));
454
+ // over the stripped lists, so a track whose only local file was its own search
455
+ // index is no longer read as one that has to be dropped
456
+ const report = analyzeWebPortability({
457
+ ...snapshot,
458
+ tracks: allTracks,
459
+ defaultSession: { ...defaultSession, sessionTracks: allSessionTracks },
460
+ });
461
+ const dropped = nonPortableUserTracks(report, droppableTrackIds(allTracks, allSessionTracks));
462
+ const keep = (t) => {
463
+ const id = readTrackId(t);
464
+ return !id || !dropped.ids.has(id);
465
+ };
466
+ const tracks = allTracks.filter(keep);
467
+ const priorSessionTracks = allSessionTracks.filter(keep);
468
+ // `base` is undefined only because the caller could not fetch
469
+ // `sourceConfigUrl` — it is the same fetch that produces it — so the two ways
470
+ // a hub drops out of the plan are distinguishable here without a flag.
471
+ const selfContainedReason = coveredByBase
472
+ ? undefined
473
+ : !sourceConfigUrl
474
+ ? 'noSourceConfig'
475
+ : baseConfig
476
+ ? 'assembliesNotInBase'
477
+ : 'baseUnreachable';
478
+ const plugins = snapshot.plugins ?? [];
479
+ const connections = snapshot.connections ?? [];
480
+ // defined exactly when the hostedConfigBase strategy applies, so it doubles as
481
+ // the strategy flag for the shared tail below
482
+ const hosted = coveredByBase
483
+ ? splitTracksAgainstBase(tracks, base.config.tracks ?? [], base.forms.track)
484
+ : undefined;
485
+ const session = withCarriedConfigs(hosted
486
+ ? withDeltas({
487
+ ...defaultSession,
488
+ ...hostedSessionAssemblies(priorSessionAssemblies, baseAssemblyNames),
489
+ sessionTracks: concatLastByKey('trackId', priorSessionTracks, hosted.addedTracks),
490
+ }, hosted.editDeltas)
491
+ : {
492
+ ...defaultSession,
493
+ sessionAssemblies: concatLastByKey('name', priorSessionAssemblies, assemblies),
494
+ sessionTracks: concatLastByKey('trackId', priorSessionTracks, tracks),
495
+ }, hosted ? extraPlugins(plugins, baseConfig?.plugins ?? []) : plugins, hosted
496
+ ? extraConnections(connections, asArray(baseConfig?.connections))
497
+ : connections);
498
+ return {
499
+ strategy: hosted ? 'hostedConfigBase' : 'selfContained',
500
+ configUrl: hosted ? sourceConfigUrl : undefined,
501
+ webBaseUrl: resolveWebBaseUrl(snapshot.configuration?.webExportUrl),
502
+ session,
503
+ droppedTracks: dropped.names,
504
+ // Re-run detection over the session that will actually ship rather than
505
+ // filtering the input report by trackId. The two are not the same set, in
506
+ // both directions: a hosted-base export leaves the config's assemblies
507
+ // behind (their local files are the base's problem, not the recipient's)
508
+ // but still ships the prior session's `sessionAssemblies`, and either
509
+ // strategy ships `connectionTrackConfigs` that the droppable-track filter
510
+ // above never touches. Anything non-portable still present here survived
511
+ // the drop and so blocks the session by definition.
512
+ blockingFiles: distinctNames(analyzeWebPortability(session).nonPortable),
513
+ selfContainedReason,
514
+ droppedTextIndexes: [
515
+ ...new Set([
516
+ ...strippedTextIndexNames(inputTracks, allTracks),
517
+ ...strippedTextIndexNames(inputSessionTracks, allSessionTracks),
518
+ ]),
519
+ ],
520
+ revertedAssemblies: coveredByBase
521
+ ? assembliesRevertedToBase(assemblies, priorSessionAssemblies, base.config.assemblies ?? [], base.forms.assembly)
522
+ : [],
523
+ unavailableAccounts: unavailableAccountIds(session, hosted ? (baseConfig?.internetAccounts ?? []) : []),
524
+ };
525
+ }
526
+ // The deployed jbrowse-web the desktop "export to web" action targets. Stable
527
+ // per the maintainers; a future target will be added alongside, not replace it.
528
+ export const DEFAULT_WEB_BASE_URL = 'https://jbrowse.org/code/jb2/latest/';
529
+ // Where an export opens. A config can point its users at a different jbrowse-web
530
+ // through the desktop-only `webExportUrl` slot — a site that runs its own
531
+ // deployment, and whose data may only be reachable from it.
532
+ //
533
+ // A value that isn't an absolute http(s) url falls back rather than throwing:
534
+ // this runs inside the export, and a typo in a config slot must not be able to
535
+ // make exporting impossible. Same posture as the base-config fetch, which logs
536
+ // and falls back to a self-contained export.
537
+ function resolveWebBaseUrl(webExportUrl) {
538
+ if (!webExportUrl) {
539
+ return DEFAULT_WEB_BASE_URL;
540
+ }
541
+ try {
542
+ const url = new URL(webExportUrl);
543
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
544
+ throw new Error(`not an http(s) url: ${webExportUrl}`);
545
+ }
546
+ // The slot names a deployment, so whatever params the value came with are
547
+ // not part of it — and the easiest way to fill the slot is to paste a
548
+ // jbrowse-web address out of the browser, which carries the pasting user's
549
+ // own `#config=…&session=…`. Params live in the hash XOR the query string
550
+ // and the hash wins whenever it holds any (queryParams.ts), so a leftover
551
+ // hash silently opens that session instead of the exported one.
552
+ url.search = '';
553
+ url.hash = '';
554
+ return url.href;
555
+ }
556
+ catch (e) {
557
+ console.error(e);
558
+ return DEFAULT_WEB_BASE_URL;
559
+ }
560
+ }
561
+ // Assembles the jbrowse-web URL for an export plan. `sessionParam` is the
562
+ // ready-made `session` value: `share-<id>` for a short lambda link (pass its
563
+ // `password`) or `encoded-<b64>`/`json-<json>` for an inline long link.
564
+ // `config` points at the hosted base, or `none` for a self-contained session.
565
+ //
566
+ // `exportedFrom` stamps what produced the link. These links are artifacts —
567
+ // papers, supplements, emails — and every other thing they name moves under
568
+ // them: DEFAULT_WEB_BASE_URL is `.../latest/`, so the deployment that opens the
569
+ // session is an unknown future build, and the hosted base config the plan
570
+ // diffed against is fetched fresh on both ends and can be in a third state by
571
+ // then. Pinning either is a deployment decision; recording the producer is not,
572
+ // and it is the difference between a recipient who can say which JBrowse made
573
+ // this and one guessing. jbrowse-web reads a fixed list of params
574
+ // (createSessionLoader) and ignores the rest, so the stamp is inert there and
575
+ // stays in the address bar where a reader can see it.
576
+ //
577
+ // Every mode goes in the hash fragment, which a browser never sends to a
578
+ // server: a self-contained export carries its own assemblies and tracks and is
579
+ // exactly the session long enough to trip the request-line limit (HTTP 414),
580
+ // and a short link's decryption key would otherwise land in the web host's
581
+ // access log. Mirrors jbrowse-web's buildShareUrl.
582
+ export function buildWebExportUrl(
583
+ // where the link points and what config it loads: the plan's report fields
584
+ // (what was dropped, what reverted) are for the sender's screen, not the URL
585
+ plan, sessionParam, options = {}) {
586
+ const url = new URL(plan.webBaseUrl);
587
+ const params = new URLSearchParams();
588
+ params.set('config', plan.configUrl ?? 'none');
589
+ params.set('session', sessionParam);
590
+ if (options.password) {
591
+ params.set('password', options.password);
592
+ }
593
+ if (options.exportedFrom) {
594
+ params.set('exportedFrom', options.exportedFrom);
595
+ }
596
+ url.search = '';
597
+ url.hash = params.toString();
598
+ return url.href;
599
+ }
@@ -0,0 +1,14 @@
1
+ import type { BaseRootModel } from './RootModel/BaseRootModel.ts';
2
+ import type { BaseSession } from './Session/BaseSession.ts';
3
+ /**
4
+ * `types.compose` can't thread the types of sibling mixins into one another, so
5
+ * a mixin that reaches members contributed by a co-composed base (`jbrowse`,
6
+ * `adminMode`, `session`, ...) must assert that base onto its own partial
7
+ * `self`. These helpers name that assertion in one place — instead of the
8
+ * ad-hoc inline `s as typeof s & BaseSession` scattered across mixins — so the
9
+ * assert target is fixed (can't drift to the wrong type) and the seam is
10
+ * greppable. The cast is unavoidable: it encodes the compose-time invariant
11
+ * that these mixins are only ever composed onto the named base.
12
+ */
13
+ export declare function asSession<S>(self: S): S & BaseSession;
14
+ export declare function asRoot<S>(self: S): S & BaseRootModel;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `types.compose` can't thread the types of sibling mixins into one another, so
3
+ * a mixin that reaches members contributed by a co-composed base (`jbrowse`,
4
+ * `adminMode`, `session`, ...) must assert that base onto its own partial
5
+ * `self`. These helpers name that assertion in one place — instead of the
6
+ * ad-hoc inline `s as typeof s & BaseSession` scattered across mixins — so the
7
+ * assert target is fixed (can't drift to the wrong type) and the seam is
8
+ * greppable. The cast is unavoidable: it encodes the compose-time invariant
9
+ * that these mixins are only ever composed onto the named base.
10
+ */
11
+ export function asSession(self) {
12
+ return self;
13
+ }
14
+ export function asRoot(self) {
15
+ return self;
16
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The two guards every loosely-typed snapshot walk in this package needs. A
3
+ * session or config snapshot arrives as `unknown` at these boundaries — it is
4
+ * `types.frozen` data, or a plain object read back off disk — so it is narrowed
5
+ * by hand rather than by MST.
6
+ */
7
+ export declare function isRecord(v: unknown): v is Record<string, unknown>;
8
+ export declare function asArray(v: unknown): unknown[];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The two guards every loosely-typed snapshot walk in this package needs. A
3
+ * session or config snapshot arrives as `unknown` at these boundaries — it is
4
+ * `types.frozen` data, or a plain object read back off disk — so it is narrowed
5
+ * by hand rather than by MST.
6
+ */
7
+ export function isRecord(v) {
8
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
9
+ }
10
+ export function asArray(v) {
11
+ return Array.isArray(v) ? v : [];
12
+ }
@@ -1,7 +1,12 @@
1
1
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
2
2
  import type { AbstractSessionModel } from '@jbrowse/core/util';
3
- export declare function AboutDialog({ config, session, handleClose, }: {
4
- config: AnyConfigurationModel | Record<string, unknown>;
3
+ export default function AboutDialog({ config, session, handleClose, }: {
5
4
  session: AbstractSessionModel;
5
+ /**
6
+ * the in-view track label passes `track.configuration`, a live node; the
7
+ * hierarchical selector passes a `session.tracks` entry, a plain object
8
+ * until something references the track
9
+ */
10
+ config: AnyConfigurationModel | Record<string, unknown>;
6
11
  handleClose: () => void;
7
- }): import("react/jsx-runtime").JSX.Element;
12
+ }): import("react").JSX.Element;