@jbrowse/product-core 4.3.0 → 5.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +629 -304
  2. package/esm/RootModel/BaseRootModel.js +139 -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 +68 -0
  8. package/esm/RootModel/PreferencesConfig.js +76 -0
  9. package/esm/RootModel/createConfigModel.d.ts +240 -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 +312 -11
  16. package/esm/Session/BaseSession.js +458 -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 +1281 -1128
  22. package/esm/Session/MultipleViews.js +396 -64
  23. package/esm/Session/Preferences.d.ts +1667 -0
  24. package/esm/Session/Preferences.js +59 -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 +1277 -1138
  28. package/esm/Session/SessionTracks.js +540 -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 +1171 -1117
  36. package/esm/Session/Tracks.js +183 -20
  37. package/esm/Session/assertSessionModel.d.ts +21 -0
  38. package/esm/Session/assertSessionModel.js +1 -0
  39. package/esm/Session/developerMode.d.ts +7 -0
  40. package/esm/Session/developerMode.js +43 -0
  41. package/esm/Session/finalizeSession.d.ts +16 -0
  42. package/esm/Session/finalizeSession.js +29 -0
  43. package/esm/Session/index.d.ts +22 -10
  44. package/esm/Session/index.js +13 -10
  45. package/esm/Session/readdedTrackConf.d.ts +8 -0
  46. package/esm/Session/readdedTrackConf.js +25 -0
  47. package/esm/Session/shareableSnapshot.d.ts +77 -0
  48. package/esm/Session/shareableSnapshot.js +196 -0
  49. package/esm/Session/temporaryAssemblyTracks.d.ts +24 -0
  50. package/esm/Session/temporaryAssemblyTracks.js +37 -0
  51. package/esm/assertExtends.d.ts +55 -0
  52. package/esm/assertExtends.js +1 -0
  53. package/esm/controllerTracks.d.ts +56 -0
  54. package/esm/controllerTracks.js +90 -0
  55. package/esm/destroyViewState.d.ts +21 -0
  56. package/esm/destroyViewState.js +29 -0
  57. package/esm/index.d.ts +31 -4
  58. package/esm/index.js +31 -4
  59. package/esm/loadPlugins.d.ts +33 -0
  60. package/esm/loadPlugins.js +41 -0
  61. package/esm/localFiles.d.ts +74 -0
  62. package/esm/localFiles.js +156 -0
  63. package/esm/observeSession.d.ts +56 -0
  64. package/esm/observeSession.js +91 -0
  65. package/esm/pluginInput.d.ts +32 -0
  66. package/esm/pluginInput.js +19 -0
  67. package/esm/pruneUnbuildableNodes.d.ts +109 -0
  68. package/esm/pruneUnbuildableNodes.js +568 -0
  69. package/esm/resolveAssemblies.d.ts +44 -0
  70. package/esm/resolveAssemblies.js +55 -0
  71. package/esm/rpcWorker.d.ts +26 -0
  72. package/esm/rpcWorker.js +98 -16
  73. package/esm/scheduleDetachedDestroy.d.ts +18 -0
  74. package/esm/scheduleDetachedDestroy.js +24 -0
  75. package/esm/sessionMigrations/index.d.ts +53 -0
  76. package/esm/sessionMigrations/index.js +432 -0
  77. package/esm/sessionUrl.d.ts +41 -0
  78. package/esm/sessionUrl.js +53 -0
  79. package/esm/sessionUtils.d.ts +69 -0
  80. package/esm/sessionUtils.js +613 -0
  81. package/esm/siblingCast.d.ts +14 -0
  82. package/esm/siblingCast.js +16 -0
  83. package/esm/snapshotUtils.d.ts +8 -0
  84. package/esm/snapshotUtils.js +12 -0
  85. package/esm/ui/AboutDialog.d.ts +3 -6
  86. package/esm/ui/AboutDialog.js +4 -4
  87. package/esm/ui/AboutDialogContents.d.ts +2 -6
  88. package/esm/ui/AboutDialogContents.js +34 -13
  89. package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
  90. package/esm/ui/AssemblyInfoPanel.js +69 -0
  91. package/esm/ui/DisplayDefaultsSection.d.ts +36 -0
  92. package/esm/ui/DisplayDefaultsSection.js +65 -0
  93. package/esm/ui/Drawer.d.ts +21 -0
  94. package/esm/ui/Drawer.js +60 -0
  95. package/esm/ui/DrawerControls.d.ts +5 -0
  96. package/esm/ui/DrawerControls.js +28 -0
  97. package/esm/ui/DrawerHeader.d.ts +6 -0
  98. package/esm/ui/DrawerHeader.js +36 -0
  99. package/esm/ui/DrawerWidget.d.ts +5 -0
  100. package/esm/ui/DrawerWidget.js +12 -0
  101. package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
  102. package/esm/ui/DrawerWidgetSelector.js +40 -0
  103. package/esm/ui/FileInfoPanel.d.ts +3 -6
  104. package/esm/ui/FileInfoPanel.js +35 -26
  105. package/esm/ui/HeaderButtons.d.ts +7 -1
  106. package/esm/ui/HeaderButtons.js +23 -18
  107. package/esm/ui/ModalWidget.d.ts +12 -0
  108. package/esm/ui/ModalWidget.js +33 -0
  109. package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
  110. package/esm/ui/ModalWidgetAppBar.js +9 -0
  111. package/esm/ui/PreferencesDialog.d.ts +56 -0
  112. package/esm/ui/PreferencesDialog.js +145 -0
  113. package/esm/ui/PreferencesResetDialog.d.ts +8 -0
  114. package/esm/ui/PreferencesResetDialog.js +19 -0
  115. package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
  116. package/esm/ui/RefNameAliasesDialog.js +65 -0
  117. package/esm/ui/RefNameInfoDialog.d.ts +3 -6
  118. package/esm/ui/RefNameInfoDialog.js +57 -63
  119. package/esm/ui/WidgetBody.d.ts +18 -0
  120. package/esm/ui/WidgetBody.js +22 -0
  121. package/esm/ui/WidgetHeading.d.ts +7 -0
  122. package/esm/ui/WidgetHeading.js +7 -0
  123. package/esm/ui/drawerLayout.d.ts +18 -0
  124. package/esm/ui/drawerLayout.js +22 -0
  125. package/esm/ui/index.d.ts +13 -1
  126. package/esm/ui/index.js +12 -1
  127. package/esm/ui/util.d.ts +37 -4
  128. package/esm/ui/util.js +35 -38
  129. package/esm/useEngineLifecycle.d.ts +19 -0
  130. package/esm/useEngineLifecycle.js +44 -0
  131. package/package.json +12 -15
  132. package/esm/RootModel/FormatAbout.d.ts +0 -12
  133. package/esm/RootModel/FormatAbout.js +0 -15
  134. package/esm/RootModel/FormatDetails.d.ts +0 -24
  135. package/esm/RootModel/FormatDetails.js +0 -27
  136. package/esm/Session/DialogQueue.d.ts +0 -17
  137. package/esm/Session/DialogQueue.js +0 -31
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Migrates old session and config snapshots to be compatible with the current
3
+ * display type registrations. Handles display types that were removed or
4
+ * renamed in the v4 rendering rearchitecture.
5
+ *
6
+ * Remapped display types (see `displayTypeMap` for the band settings each one
7
+ * carries over):
8
+ * LinearPileupDisplay → LinearAlignmentsDisplay
9
+ * LinearSNPCoverageDisplay → LinearAlignmentsDisplay
10
+ * LinearReadArcsDisplay → LinearAlignmentsDisplay
11
+ * LinearReadCloudDisplay → LinearAlignmentsDisplay
12
+ * LinearFeatureDisplay → LinearBasicDisplay
13
+ *
14
+ * Also lifts the per-instance alignments settings a v4.3.0 session persisted
15
+ * (`colorBySetting`, `filterBySetting`, `jexlFilters`, `trackMaxHeight`,
16
+ * `hideMismatchesSetting`) into the config where they now live as slots — off
17
+ * the nested `LinearAlignmentsDisplay` container's `PileupDisplay` /
18
+ * `SNPCoverageDisplay` sub-nodes and off a flat old display alike, see
19
+ * `extractAlignmentsInstanceSettings` — and routes the legacy per-instance
20
+ * `heightPreConfig` display-height prop onto the `height` config slot — see
21
+ * `extractInstanceHeight`.
22
+ */
23
+ // Each old display drew one band; the unified LinearAlignmentsDisplay draws all
24
+ // of them, so a bare type rename turns a coverage-only track into coverage +
25
+ // pileup and an arcs track into a plain pileup. `settings` restores the old
26
+ // intent through the config slots that now express it, and is applied only where
27
+ // the snapshot doesn't already carry the key, so an explicit author value wins.
28
+ const displayTypeMap = {
29
+ LinearPileupDisplay: {
30
+ type: 'LinearAlignmentsDisplay',
31
+ settings: { showCoverage: false },
32
+ },
33
+ LinearSNPCoverageDisplay: {
34
+ type: 'LinearAlignmentsDisplay',
35
+ // coverageHeight + height together, else the band keeps its 45px default
36
+ // under the 250px display height and leaves 200px blank.
37
+ settings: { showPileup: false, coverageHeight: 100, height: 100 },
38
+ },
39
+ LinearReadArcsDisplay: {
40
+ type: 'LinearAlignmentsDisplay',
41
+ settings: {
42
+ showPileup: false,
43
+ showCoverage: false,
44
+ readConnections: 'arc',
45
+ },
46
+ },
47
+ LinearReadCloudDisplay: {
48
+ type: 'LinearAlignmentsDisplay',
49
+ settings: {
50
+ showPileup: false,
51
+ showCoverage: false,
52
+ readConnections: 'cloud',
53
+ },
54
+ },
55
+ LinearFeatureDisplay: { type: 'LinearBasicDisplay' },
56
+ };
57
+ // The pre-4.x LinearAlignmentsDisplay was a container whose per-instance
58
+ // track-menu settings lived on nested `PileupDisplay` / `SNPCoverageDisplay`
59
+ // sub-nodes. Those settings are now config slots on the flat
60
+ // LinearAlignmentsDisplay, so the sub-nodes are dead on load — MST drops them
61
+ // and `colorBy`/`filterBy` silently revert to their config default (e.g. a
62
+ // modifications/methylation session opens colored `normal`). We pull them off
63
+ // the instance here and route them into the config.
64
+ const NESTED_ALIGNMENTS_SUBNODES = ['PileupDisplay', 'SNPCoverageDisplay'];
65
+ // Persisted key -> the slot it now lives in, and how to carry its value across.
66
+ //
67
+ // The `*Setting` names are the ones a real saved session carries, and getting
68
+ // them wrong is why this migration never fired: SharedLinearPileupDisplayMixin
69
+ // (v4.3.0) declared the props as `colorBySetting`/`filterBySetting`, its
70
+ // `preProcessSnapshot` renamed an incoming bare `colorBy` to `colorBySetting`,
71
+ // and its `postProcessSnapshot` wrote that name back out — so no session
72
+ // snapshot JBrowse ever produced holds the bare form this used to look for. The
73
+ // bare names stay accepted because a hand-written snapshot may use them.
74
+ //
75
+ // `hideSmallIndelsSetting` and `hideLargeIndelsSetting` are deliberately absent:
76
+ // those two toggles have no slot today, the feature having been removed rather
77
+ // than moved, so there is nowhere to carry them.
78
+ const MIGRATED_INSTANCE_SLOTS = {
79
+ colorBy: { slot: 'colorBy' },
80
+ colorBySetting: { slot: 'colorBy' },
81
+ filterBy: { slot: 'filterBy' },
82
+ filterBySetting: { slot: 'filterBy' },
83
+ jexlFilters: { slot: 'jexlFilters' },
84
+ trackMaxHeight: { slot: 'maxHeight' },
85
+ hideMismatchesSetting: {
86
+ slot: 'showMismatches',
87
+ convert: value => !value,
88
+ },
89
+ };
90
+ /**
91
+ * Every display-instance key a session snapshot may still carry and have
92
+ * honored, keyed by the display type it applies to — `*` for the ones any
93
+ * display gets. A snapshot carrying one of these loads correctly, so `jbrowse
94
+ * validate` reports it as stale rather than dead; that command reads this map
95
+ * (through the generated config manifest) rather than keeping a copy that could
96
+ * drift.
97
+ *
98
+ * Not every entry is lifted *here*. The two multi-sample variant displays lift
99
+ * their own `jexlFilters` in the model's `preProcessSnapshot`, because the value
100
+ * stays on the instance (under `jexlFiltersSetting`) rather than moving to a
101
+ * config slot, which is the only shape this module handles. What decides
102
+ * membership is whether the key still works, not which file makes it work.
103
+ *
104
+ * Keyed rather than flat because the same name means different things on
105
+ * different displays: `jexlFilters` is lifted for the alignments display and,
106
+ * differently, for the two variant ones, while on a LinearBasicDisplay — whose
107
+ * own prop has always been `jexlFiltersSetting` — it is simply dead, and a flat
108
+ * list would call that one supported.
109
+ */
110
+ export const MIGRATED_DISPLAY_INSTANCE_KEYS = {
111
+ '*': ['heightPreConfig'],
112
+ LinearAlignmentsDisplay: [
113
+ ...Object.keys(MIGRATED_INSTANCE_SLOTS),
114
+ ...NESTED_ALIGNMENTS_SUBNODES,
115
+ ],
116
+ LinearMultiSampleVariantDisplay: ['jexlFilters'],
117
+ LinearMultiSampleVariantMatrixDisplay: ['jexlFilters'],
118
+ };
119
+ // Read the migrated settings off one node (a nested sub-node, or an old flat
120
+ // display's own snapshot), keyed by the slot they now belong to. A key present
121
+ // under both its bare and its `*Setting` spelling resolves to whichever comes
122
+ // last in MIGRATED_INSTANCE_SLOTS, which is the `*Setting` one — the spelling
123
+ // the display was actually writing.
124
+ function migratedSettingsOf(source) {
125
+ const settings = {};
126
+ for (const [key, { slot, convert }] of Object.entries(MIGRATED_INSTANCE_SLOTS)) {
127
+ if (source[key] !== undefined) {
128
+ settings[slot] = convert ? convert(source[key]) : source[key];
129
+ }
130
+ }
131
+ return settings;
132
+ }
133
+ function isObject(v) {
134
+ return !!v && typeof v === 'object' && !Array.isArray(v);
135
+ }
136
+ // The standalone `methylation` colorBy scheme was removed; it is now the
137
+ // `modifications` scheme with `fillUnmarked` set. It is NOT rewritten here: the
138
+ // alignments model normalizes it at read time (`normalizeColorBy`, applied in
139
+ // its `colorBy` getter), which covers every persistence path — instance slot,
140
+ // session-wide promoted default, and config-file display default — so session
141
+ // migration deliberately leaves a `methylation` colorBy untouched.
142
+ function migrateDisplayType(display, trackConfigId, collected) {
143
+ const entry = displayTypeMap[display.type];
144
+ if (!entry) {
145
+ return display;
146
+ }
147
+ const missing = Object.fromEntries(Object.entries(entry.settings ?? {}).filter(([k]) => display[k] === undefined));
148
+ // Every one of these is a config slot on the flat display, and a session
149
+ // instance declares only `type` and `configuration` — so merging them here
150
+ // would hand MST properties it drops on load, and the compensation would be
151
+ // gone exactly where it is needed most: a v4 session on an admin config, which
152
+ // is the commonest way anyone has one of these displays at all. Route them the
153
+ // way extractInstanceHeight routes its own, into the config the instance
154
+ // points at. A *config* display node has no `configuration` reference and
155
+ // keeps them inline, where they are the live slots.
156
+ const displayId = display.configuration;
157
+ if (trackConfigId && typeof displayId === 'string') {
158
+ if (Object.keys(missing).length > 0) {
159
+ collected.push({
160
+ trackConfigId,
161
+ displayId,
162
+ displayType: entry.type,
163
+ settings: missing,
164
+ });
165
+ }
166
+ return { ...display, type: entry.type };
167
+ }
168
+ return { ...display, ...missing, type: entry.type };
169
+ }
170
+ /**
171
+ * Pull the per-instance alignments settings off an old display into `collected`
172
+ * and return the display with the dead keys stripped. The caller routes
173
+ * `collected` into the config (see `applyExtractedSettings`); the settings can't
174
+ * stay on the instance because they're config slots now.
175
+ *
176
+ * Two shapes, because v4.3.0 sessions hold both:
177
+ *
178
+ * - the nested `LinearAlignmentsDisplay` container, whose settings sat on
179
+ * `PileupDisplay` / `SNPCoverageDisplay` sub-nodes
180
+ * - a flat `LinearPileupDisplay` / `LinearReadArcsDisplay` /
181
+ * `LinearReadCloudDisplay` / `LinearSNPCoverageDisplay`, which carried them
182
+ * directly — `migrateDisplayType` has already renamed it by the time this
183
+ * runs, so both arrive here as a LinearAlignmentsDisplay
184
+ *
185
+ * The display's own keys are read only when it carries a `configuration`
186
+ * reference, i.e. only for a session instance. A *config* display node reaches
187
+ * this same function through `migrateConfigSnapshot`, and there `colorBy` and
188
+ * friends are the live slots — lifting them would strip a working config.
189
+ */
190
+ function extractAlignmentsInstanceSettings(display, trackConfigId, collected) {
191
+ if (display.type !== 'LinearAlignmentsDisplay') {
192
+ return display;
193
+ }
194
+ const subNode = NESTED_ALIGNMENTS_SUBNODES.map(k => display[k]).find(isObject);
195
+ // The instance's `configuration` string is the display config id the settings
196
+ // must merge onto (`${trackId}-LinearAlignmentsDisplay`). Skip routing if it's
197
+ // inline or the track id is unknown — nothing to key the merge on.
198
+ const displayId = display.configuration;
199
+ const isInstance = typeof displayId === 'string';
200
+ const ownSettings = isInstance ? migratedSettingsOf(display) : {};
201
+ const settings = {
202
+ ...(subNode ? migratedSettingsOf(subNode) : {}),
203
+ ...ownSettings,
204
+ };
205
+ if (!subNode && Object.keys(settings).length === 0) {
206
+ return display;
207
+ }
208
+ if (trackConfigId && isInstance && Object.keys(settings).length > 0) {
209
+ collected.push({
210
+ trackConfigId,
211
+ displayId,
212
+ displayType: 'LinearAlignmentsDisplay',
213
+ settings,
214
+ });
215
+ }
216
+ const { PileupDisplay, SNPCoverageDisplay, ...rest } = display;
217
+ if (isInstance) {
218
+ // Every one of these is dead on a display instance now, routed or not — MST
219
+ // would drop them on load anyway, and leaving them makes a migrated snapshot
220
+ // still look like it holds settings it does not.
221
+ for (const key of Object.keys(MIGRATED_INSTANCE_SLOTS)) {
222
+ delete rest[key];
223
+ }
224
+ }
225
+ return rest;
226
+ }
227
+ // Pre-v4 the display height lived in a per-instance `heightPreConfig` MST
228
+ // prop (`height = heightPreConfig ?? config-height`); the drag-resize handle
229
+ // wrote it. That prop is gone — the height is now the `height` config slot only
230
+ // — so an old session's `heightPreConfig` is silently dropped on load and the
231
+ // track snaps to the config default (e.g. the alignments 250px). Route it onto
232
+ // the display's `height` slot so saved/shared sessions keep their heights, and
233
+ // strip the dead prop. When a track shows in two panels (a synteny view's two
234
+ // LGVs share one display config), the last panel's value wins — both then render
235
+ // at one height, which is the config-slot model's intent.
236
+ function extractInstanceHeight(display, trackConfigId, collected) {
237
+ if (typeof display.heightPreConfig !== 'number') {
238
+ return display;
239
+ }
240
+ const displayId = display.configuration;
241
+ if (trackConfigId && typeof displayId === 'string') {
242
+ collected.push({
243
+ trackConfigId,
244
+ displayId,
245
+ displayType: typeof display.type === 'string' ? display.type : undefined,
246
+ settings: { height: display.heightPreConfig },
247
+ });
248
+ }
249
+ const { heightPreConfig, ...rest } = display;
250
+ return rest;
251
+ }
252
+ function migrateDisplaySnapshot(display, trackConfigId, collected) {
253
+ return extractInstanceHeight(extractAlignmentsInstanceSettings(migrateDisplayType(display, trackConfigId, collected), trackConfigId, collected), trackConfigId, collected);
254
+ }
255
+ function migrateDisplaysArray(displays, trackConfigId, collected) {
256
+ let changed = false;
257
+ const result = displays.map(d => {
258
+ if (isObject(d) && 'type' in d) {
259
+ const migrated = migrateDisplaySnapshot(d, trackConfigId, collected);
260
+ if (migrated !== d) {
261
+ changed = true;
262
+ }
263
+ return migrated;
264
+ }
265
+ return d;
266
+ });
267
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
268
+ return changed ? result : displays;
269
+ }
270
+ function migrateTrackSnapshot(track, collected) {
271
+ if (!isObject(track)) {
272
+ return track;
273
+ }
274
+ let changed = false;
275
+ const result = { ...track };
276
+ const displays = track.displays;
277
+ if (Array.isArray(displays)) {
278
+ const trackConfigId = typeof track.configuration === 'string' ? track.configuration : undefined;
279
+ const newDisplays = migrateDisplaysArray(displays, trackConfigId, collected);
280
+ if (newDisplays !== displays) {
281
+ changed = true;
282
+ result.displays = newDisplays;
283
+ }
284
+ }
285
+ return changed ? result : track;
286
+ }
287
+ function migrateViewSnapshot(view, collected) {
288
+ let changed = false;
289
+ const result = { ...view };
290
+ const tracks = view.tracks;
291
+ if (Array.isArray(tracks)) {
292
+ const newTracks = tracks.map(t => migrateTrackSnapshot(t, collected));
293
+ if (newTracks.some((t, i) => t !== tracks[i])) {
294
+ changed = true;
295
+ result.tracks = newTracks;
296
+ }
297
+ }
298
+ // Handle nested views (e.g. LinearSyntenyView has sub-views)
299
+ const subViews = view.views;
300
+ if (Array.isArray(subViews)) {
301
+ const newSubViews = subViews.map(sv => isObject(sv) ? migrateViewSnapshot(sv, collected) : sv);
302
+ if (newSubViews.some((v, i) => v !== subViews[i])) {
303
+ changed = true;
304
+ result.views = newSubViews;
305
+ }
306
+ }
307
+ return changed ? result : view;
308
+ }
309
+ // Merge `settings` onto the display config carrying `displayId` inside `track`
310
+ // (an in-session track config). Returns a new track if it changed. The config
311
+ // slots win over whatever the old config held.
312
+ function mergeSettingsIntoTrackConfig(track, displayId, displayType, settings) {
313
+ const displays = track.displays;
314
+ const list = Array.isArray(displays) ? displays : [];
315
+ const hasMatch = list.some(d => isObject(d) && d.displayId === displayId);
316
+ const newDisplays = hasMatch
317
+ ? list.map(d => isObject(d) && d.displayId === displayId ? { ...d, ...settings } : d)
318
+ : [
319
+ ...list,
320
+ {
321
+ type: displayType ?? 'LinearAlignmentsDisplay',
322
+ displayId,
323
+ ...settings,
324
+ },
325
+ ];
326
+ return { ...track, displays: newDisplays };
327
+ }
328
+ /**
329
+ * Route each setting lifted off a nested alignments instance into the config it
330
+ * now lives in: a user-added `sessionTracks` entry is edited in place (the
331
+ * config is embedded there), while an admin config track — whose base lives in
332
+ * the reloaded config file, out of reach here — gets a `trackConfigDeltas`
333
+ * entry keyed by trackId (see product-core/src/Session/CLAUDE.md).
334
+ */
335
+ function applyExtractedSettings(snapshot, collected) {
336
+ if (collected.length === 0) {
337
+ return snapshot;
338
+ }
339
+ const sessionTracks = Array.isArray(snapshot.sessionTracks)
340
+ ? [...snapshot.sessionTracks]
341
+ : [];
342
+ const sessionTrackIndex = new Map(sessionTracks.map((t, i) => [isObject(t) ? t.trackId : undefined, i]));
343
+ const deltas = isObject(snapshot.trackConfigDeltas)
344
+ ? { ...snapshot.trackConfigDeltas }
345
+ : {};
346
+ let sessionTracksChanged = false;
347
+ let deltasChanged = false;
348
+ for (const { trackConfigId, displayId, displayType, settings } of collected) {
349
+ const idx = sessionTrackIndex.get(trackConfigId);
350
+ if (idx !== undefined && isObject(sessionTracks[idx])) {
351
+ sessionTracks[idx] = mergeSettingsIntoTrackConfig(sessionTracks[idx], displayId, displayType, settings);
352
+ sessionTracksChanged = true;
353
+ }
354
+ else {
355
+ const existing = isObject(deltas[trackConfigId])
356
+ ? deltas[trackConfigId]
357
+ : { trackId: trackConfigId };
358
+ deltas[trackConfigId] = mergeSettingsIntoTrackConfig(existing, displayId, displayType, settings);
359
+ deltasChanged = true;
360
+ }
361
+ }
362
+ return {
363
+ ...snapshot,
364
+ ...(sessionTracksChanged ? { sessionTracks } : {}),
365
+ ...(deltasChanged ? { trackConfigDeltas: deltas } : {}),
366
+ };
367
+ }
368
+ /**
369
+ * Walks a session snapshot and remaps old display types to their new names.
370
+ * Handles displays nested inside views, tracks, and sessionTracks.
371
+ */
372
+ export function migrateSessionSnapshot(snapshot) {
373
+ let changed = false;
374
+ const result = { ...snapshot };
375
+ // Per-instance alignments settings lifted off old nested displays in views,
376
+ // routed into the config after the walk (they can't stay on the instance).
377
+ const collected = [];
378
+ const views = snapshot.views;
379
+ if (Array.isArray(views)) {
380
+ const newViews = views.map(view => isObject(view) ? migrateViewSnapshot(view, collected) : view);
381
+ if (newViews.some((v, i) => v !== views[i])) {
382
+ changed = true;
383
+ result.views = newViews;
384
+ }
385
+ }
386
+ const sessionTracks = snapshot.sessionTracks;
387
+ if (Array.isArray(sessionTracks)) {
388
+ const newTracks = sessionTracks.map(t => migrateTrackSnapshot(t, collected));
389
+ if (newTracks.some((t, i) => t !== sessionTracks[i])) {
390
+ changed = true;
391
+ result.sessionTracks = newTracks;
392
+ }
393
+ }
394
+ // trackConfigDeltas is a `trackId → partial track config` map (see
395
+ // SessionTracks.ts). A delta carries a `displays` array like a track, so run
396
+ // each through the same track migrator; migrateTrackSnapshot is a no-op for a
397
+ // delta that carries no stale display type.
398
+ const deltas = snapshot.trackConfigDeltas;
399
+ if (deltas && typeof deltas === 'object') {
400
+ let deltasChanged = false;
401
+ const newDeltas = {};
402
+ for (const [trackId, delta] of Object.entries(deltas)) {
403
+ const migrated = migrateTrackSnapshot(delta, collected);
404
+ if (migrated !== delta) {
405
+ deltasChanged = true;
406
+ }
407
+ newDeltas[trackId] = migrated;
408
+ }
409
+ if (deltasChanged) {
410
+ changed = true;
411
+ result.trackConfigDeltas = newDeltas;
412
+ }
413
+ }
414
+ // applyExtractedSettings returns its input by identity when nothing was
415
+ // collected, so this preserves the unchanged-by-identity contract.
416
+ return applyExtractedSettings(changed ? result : snapshot, collected);
417
+ }
418
+ /**
419
+ * Walks a config snapshot and remaps old display types in track definitions.
420
+ * Config tracks have displays in their configuration (not in views).
421
+ */
422
+ export function migrateConfigSnapshot(snapshot) {
423
+ const tracks = snapshot.tracks;
424
+ if (!Array.isArray(tracks)) {
425
+ return snapshot;
426
+ }
427
+ const newTracks = tracks.map(t => migrateTrackSnapshot(t, []));
428
+ if (newTracks.every((t, i) => t === tracks[i])) {
429
+ return snapshot;
430
+ }
431
+ return { ...snapshot, tracks: newTracks };
432
+ }
@@ -0,0 +1,41 @@
1
+ import type { AbstractSessionModel } from '@jbrowse/core/util';
2
+ /**
3
+ * A serialized session. Open-shaped on purpose: the concrete shape is the
4
+ * session model's `SnapshotIn`, which grows with every plugin a host loads.
5
+ */
6
+ export interface SessionSnapshot {
7
+ name: string;
8
+ [key: string]: unknown;
9
+ }
10
+ /**
11
+ * Serialize a live session into a compact, URL-safe string for a query param or
12
+ * hash fragment. Deflated then base64url-encoded, so it survives a URL intact
13
+ * and stays far smaller than raw JSON.
14
+ *
15
+ * Not a plain `getSnapshot`: display settings a user is *inheriting* from a
16
+ * promoted display-type default live in their own browser, never in the
17
+ * session, so a raw snapshot renders differently for whoever opens the link.
18
+ * `getShareableSessionSnapshot` flattens that cascade into the snapshot first.
19
+ *
20
+ * The session travels but the config does not — the receiving app supplies its
21
+ * own assembly/tracks. Put the result somewhere the server never sees (the hash
22
+ * fragment) if it may be long: a query string can exceed the request-line limit
23
+ * and get a 414, which is why jbrowse-web moved its own there.
24
+ *
25
+ * Carries `ENCODED_PREFIX`, the same `?session=encoded-…` value jbrowse-web's
26
+ * SessionLoader decodes, so a link built by an embedded product opens in
27
+ * jbrowse.org/jb2 and vice versa. The format is `encodeSessionParam`'s `long`
28
+ * mode; sessionUrl.test.ts asserts the two stay byte-identical.
29
+ */
30
+ export declare function encodeSessionToUrl(session: AbstractSessionModel): Promise<string>;
31
+ /**
32
+ * Inverse of {@link encodeSessionToUrl}. Accepts the value with or without the
33
+ * `encoded-` prefix, so a raw `?session=` value from either app passes straight
34
+ * through.
35
+ *
36
+ * Throws on anything that isn't a decodable session — a truncated link, or a
37
+ * `share-`/`spec-` param an embedded product doesn't handle — so a host can
38
+ * fall back to its own declarative launch state instead of opening a
39
+ * half-built session.
40
+ */
41
+ export declare function decodeSessionFromUrl(encoded: string): Promise<SessionSnapshot>;
@@ -0,0 +1,53 @@
1
+ import { ENCODED_PREFIX, fromUrlSafeB64, toUrlSafeB64, } from '@jbrowse/core/util';
2
+ import { getShareableSessionSnapshot } from './Session/index.js';
3
+ /**
4
+ * Serialize a live session into a compact, URL-safe string for a query param or
5
+ * hash fragment. Deflated then base64url-encoded, so it survives a URL intact
6
+ * and stays far smaller than raw JSON.
7
+ *
8
+ * Not a plain `getSnapshot`: display settings a user is *inheriting* from a
9
+ * promoted display-type default live in their own browser, never in the
10
+ * session, so a raw snapshot renders differently for whoever opens the link.
11
+ * `getShareableSessionSnapshot` flattens that cascade into the snapshot first.
12
+ *
13
+ * The session travels but the config does not — the receiving app supplies its
14
+ * own assembly/tracks. Put the result somewhere the server never sees (the hash
15
+ * fragment) if it may be long: a query string can exceed the request-line limit
16
+ * and get a 414, which is why jbrowse-web moved its own there.
17
+ *
18
+ * Carries `ENCODED_PREFIX`, the same `?session=encoded-…` value jbrowse-web's
19
+ * SessionLoader decodes, so a link built by an embedded product opens in
20
+ * jbrowse.org/jb2 and vice versa. The format is `encodeSessionParam`'s `long`
21
+ * mode; sessionUrl.test.ts asserts the two stay byte-identical.
22
+ */
23
+ export async function encodeSessionToUrl(session) {
24
+ const snap = getShareableSessionSnapshot(session);
25
+ return `${ENCODED_PREFIX}${await toUrlSafeB64(JSON.stringify(snap))}`;
26
+ }
27
+ function isSessionSnapshot(value) {
28
+ return (typeof value === 'object' &&
29
+ value !== null &&
30
+ !Array.isArray(value) &&
31
+ 'name' in value &&
32
+ typeof value.name === 'string');
33
+ }
34
+ /**
35
+ * Inverse of {@link encodeSessionToUrl}. Accepts the value with or without the
36
+ * `encoded-` prefix, so a raw `?session=` value from either app passes straight
37
+ * through.
38
+ *
39
+ * Throws on anything that isn't a decodable session — a truncated link, or a
40
+ * `share-`/`spec-` param an embedded product doesn't handle — so a host can
41
+ * fall back to its own declarative launch state instead of opening a
42
+ * half-built session.
43
+ */
44
+ export async function decodeSessionFromUrl(encoded) {
45
+ const b64 = encoded.startsWith(ENCODED_PREFIX)
46
+ ? encoded.slice(ENCODED_PREFIX.length)
47
+ : encoded;
48
+ const parsed = JSON.parse(await fromUrlSafeB64(b64));
49
+ if (!isSessionSnapshot(parsed)) {
50
+ throw new Error('not a session snapshot: no "name"');
51
+ }
52
+ return parsed;
53
+ }
@@ -0,0 +1,69 @@
1
+ import type { PluginDefinition } from '@jbrowse/core/pluginDefinitions';
2
+ import type { IAnyStateTreeNode, IAnyType } from '@jbrowse/mobx-state-tree';
3
+ export interface DroppedSessionNode {
4
+ type?: string;
5
+ configuration?: string;
6
+ }
7
+ export declare function filterSessionInPlace(node: IAnyStateTreeNode, type: IAnyType, dropped?: DroppedSessionNode[]): DroppedSessionNode[];
8
+ export interface NonPortableLocation {
9
+ locationType: string;
10
+ name: string;
11
+ trackId?: string;
12
+ trackName?: string;
13
+ }
14
+ export interface WebPortabilityReport {
15
+ nonPortable: NonPortableLocation[];
16
+ portable: boolean;
17
+ }
18
+ export declare function analyzeWebPortability(snapshot: unknown): WebPortabilityReport;
19
+ export interface TrackSnapshot {
20
+ trackId: string;
21
+ [key: string]: unknown;
22
+ }
23
+ export interface WebExportInput {
24
+ assemblies?: AssemblySnapshot[];
25
+ tracks?: TrackSnapshot[];
26
+ plugins?: PluginDefinition[];
27
+ connections?: unknown[];
28
+ configuration?: {
29
+ sourceConfigUrl?: string;
30
+ webExportUrl?: string;
31
+ };
32
+ defaultSession?: Record<string, unknown>;
33
+ }
34
+ export interface AssemblySnapshot {
35
+ name: string;
36
+ [key: string]: unknown;
37
+ }
38
+ export interface HostedBaseConfig {
39
+ assemblies?: AssemblySnapshot[];
40
+ tracks?: TrackSnapshot[];
41
+ internetAccounts?: {
42
+ internetAccountId?: string;
43
+ }[];
44
+ plugins?: PluginDefinition[];
45
+ connections?: unknown[];
46
+ configuration?: {
47
+ shareURL?: string;
48
+ };
49
+ [key: string]: unknown;
50
+ }
51
+ export interface WebExportPlan {
52
+ strategy: 'hostedConfigBase' | 'selfContained';
53
+ configUrl?: string;
54
+ webBaseUrl: string;
55
+ session: Record<string, unknown>;
56
+ droppedTracks: string[];
57
+ blockingFiles: string[];
58
+ selfContainedReason?: SelfContainedReason;
59
+ droppedTextIndexes: string[];
60
+ revertedAssemblies: string[];
61
+ unavailableAccounts: string[];
62
+ }
63
+ export type SelfContainedReason = 'noSourceConfig' | 'baseUnreachable' | 'assembliesNotInBase';
64
+ export declare function planWebExport(snapshot: WebExportInput, baseConfig?: HostedBaseConfig): WebExportPlan;
65
+ export declare const DEFAULT_WEB_BASE_URL = "https://jbrowse.org/code/jb2/latest/";
66
+ export declare function buildWebExportUrl(plan: Pick<WebExportPlan, 'webBaseUrl' | 'configUrl'>, sessionParam: string, options?: {
67
+ password?: string;
68
+ exportedFrom?: string;
69
+ }): string;