@jbrowse/core 5.0.0-beta.2 → 5.0.0-beta.3

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 (140) hide show
  1. package/README.md +70 -40
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/FeatureDetails.d.ts +2 -3
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/ParentFeatureLine.d.ts +4 -0
  4. package/esm/BaseFeatureWidget/BaseFeatureDetail/ParentFeatureLine.js +17 -0
  5. package/esm/BaseFeatureWidget/BaseFeatureDetail/index.js +4 -3
  6. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.d.ts +2 -2
  7. package/esm/BaseFeatureWidget/index.d.ts +1 -1
  8. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +8 -1
  9. package/esm/BaseFeatureWidget/stateModelFactory.js +6 -0
  10. package/esm/BaseFeatureWidget/types.d.ts +10 -2
  11. package/esm/PluginManager.d.ts +30 -1
  12. package/esm/PluginManager.js +107 -70
  13. package/esm/ReExports/BaseFeatureDetails.js +1 -1
  14. package/esm/ReExports/MuiDataGridReExports.d.ts +1 -0
  15. package/esm/ReExports/MuiDataGridReExports.js +2 -2
  16. package/esm/ReExports/barrelOnlyNames.d.ts +5 -0
  17. package/esm/ReExports/barrelOnlyNames.js +63 -0
  18. package/esm/ReExports/modules.js +28 -56
  19. package/esm/ReExports/publicTracks.d.ts +1 -0
  20. package/esm/ReExports/publicTracks.js +14 -0
  21. package/esm/ReExports/publicUi.d.ts +4 -0
  22. package/esm/ReExports/publicUi.js +33 -0
  23. package/esm/ReExports/publicUtil.d.ts +1 -1
  24. package/esm/ReExports/publicUtil.js +8 -4
  25. package/esm/ReExports/sharedModules.d.ts +1 -1
  26. package/esm/ReExports/sharedModules.js +8 -1
  27. package/esm/ReExports/uiStub.d.ts +1 -0
  28. package/esm/ReExports/uiStub.js +6 -8
  29. package/esm/ReExports/workerModules.js +9 -4
  30. package/esm/ReExports/workerNamespaceNames.d.ts +11 -0
  31. package/esm/ReExports/workerNamespaceNames.js +281 -0
  32. package/esm/configuration/configurationSchema.js +11 -0
  33. package/esm/configuration/index.d.ts +2 -2
  34. package/esm/configuration/index.js +1 -1
  35. package/esm/configuration/promotableDefaults.d.ts +54 -19
  36. package/esm/configuration/promotableDefaults.js +108 -45
  37. package/esm/configuration/promotablePin.d.ts +50 -40
  38. package/esm/configuration/promotableResolve.d.ts +1 -1
  39. package/esm/configuration/promotableResolve.js +1 -1
  40. package/esm/configuration/tabixIndexFields.d.ts +2 -2
  41. package/esm/configuration/tabixIndexFields.js +2 -2
  42. package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.js +4 -12
  43. package/esm/data_adapters/BaseAdapter/featureDensity.d.ts +12 -0
  44. package/esm/data_adapters/BaseAdapter/featureDensity.js +15 -0
  45. package/esm/data_adapters/BaseAdapter/index.d.ts +2 -2
  46. package/esm/data_adapters/BaseAdapter/index.js +1 -1
  47. package/esm/data_adapters/BaseAdapter/types.d.ts +2 -8
  48. package/esm/pluggableElementTypes/AdapterType.d.ts +2 -1
  49. package/esm/pluggableElementTypes/AdapterType.js +1 -1
  50. package/esm/pluggableElementTypes/DisplayType.d.ts +3 -10
  51. package/esm/pluggableElementTypes/DisplayType.js +3 -45
  52. package/esm/pluggableElementTypes/LazyStateModelElement.d.ts +27 -0
  53. package/esm/pluggableElementTypes/LazyStateModelElement.js +71 -0
  54. package/esm/pluggableElementTypes/ViewType.d.ts +3 -10
  55. package/esm/pluggableElementTypes/ViewType.js +6 -51
  56. package/esm/pluggableElementTypes/extendElementType.d.ts +1 -2
  57. package/esm/pluggableElementTypes/extendElementType.js +1 -2
  58. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +3 -3
  59. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +1 -1
  60. package/esm/pluggableElementTypes/models/BaseTrackModel.js +1 -4
  61. package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +34 -34
  62. package/esm/pluggableElementTypes/models/BaseViewModel.js +19 -30
  63. package/esm/pluggableElementTypes/models/assertDisplayContract.d.ts +4 -3
  64. package/esm/pluggableElementTypes/models/assertDisplayContract.js +2 -2
  65. package/esm/pluggableElementTypes/models/baseTrackConfig.js +7 -1
  66. package/esm/ui/BaseExportSvgDialog.d.ts +1 -1
  67. package/esm/ui/ErrorMessageStackTraceDialog.js +5 -6
  68. package/esm/ui/ExportSvgDialog.d.ts +1 -1
  69. package/esm/ui/MenuTypes.d.ts +3 -3
  70. package/esm/ui/PinAdornment.js +22 -40
  71. package/esm/ui/ScrollChrome.d.ts +16 -0
  72. package/esm/ui/ScrollChrome.js +12 -0
  73. package/esm/ui/ScrollZoomToggle.d.ts +4 -3
  74. package/esm/ui/ScrollZoomToggle.js +4 -3
  75. package/esm/ui/SizeSliderRow.js +1 -1
  76. package/esm/ui/index.d.ts +2 -2
  77. package/esm/ui/index.js +3 -3
  78. package/esm/ui/legendMenuItem.d.ts +9 -15
  79. package/esm/ui/legendMenuItem.js +8 -19
  80. package/esm/ui/menuItemAdornment.d.ts +2 -3
  81. package/esm/ui/menuItemAdornment.js +2 -3
  82. package/esm/ui/menuItems.d.ts +1 -2
  83. package/esm/ui/menuItems.js +0 -1
  84. package/esm/ui/palette.d.ts +11 -11
  85. package/esm/ui/palette.js +11 -11
  86. package/esm/ui/promotablePinCoverage.d.ts +16 -2
  87. package/esm/ui/promotablePinCoverage.js +38 -2
  88. package/esm/ui/theme.js +5 -0
  89. package/esm/ui/toggleMenuItems.d.ts +13 -6
  90. package/esm/ui/toggleMenuItems.js +32 -15
  91. package/esm/util/Base1DUtils.d.ts +4 -2
  92. package/esm/util/Base1DUtils.js +12 -4
  93. package/esm/util/SvgCanvas.js +4 -3
  94. package/esm/util/adapterConfigKey.d.ts +12 -0
  95. package/esm/util/adapterConfigKey.js +14 -0
  96. package/esm/util/adapterMetadata.js +2 -1
  97. package/esm/util/analytics.js +4 -5
  98. package/esm/util/clampToContig.d.ts +17 -0
  99. package/esm/util/clampToContig.js +19 -0
  100. package/esm/util/colorBits.d.ts +2 -1
  101. package/esm/util/colorBits.js +2 -5
  102. package/esm/util/createStopTokenRotation.d.ts +8 -7
  103. package/esm/util/featureTypes.d.ts +24 -0
  104. package/esm/util/featureTypes.js +35 -0
  105. package/esm/util/fetchPhases.d.ts +1 -1
  106. package/esm/util/index.d.ts +4 -2
  107. package/esm/util/index.js +3 -1
  108. package/esm/util/installFetch.d.ts +29 -18
  109. package/esm/util/installFetch.js +17 -17
  110. package/esm/util/installInitAutorun.d.ts +6 -5
  111. package/esm/util/installInitAutorun.js +2 -2
  112. package/esm/util/installPrerequisiteFetch.d.ts +86 -0
  113. package/esm/util/installPrerequisiteFetch.js +73 -0
  114. package/esm/util/openFeatureWidget.d.ts +5 -0
  115. package/esm/util/openFeatureWidget.js +1 -0
  116. package/esm/util/openViews.d.ts +16 -0
  117. package/esm/util/openViews.js +22 -0
  118. package/esm/util/parentWalk.d.ts +1 -1
  119. package/esm/util/parentWalk.js +1 -1
  120. package/esm/util/renderToStaticMarkup.js +10 -0
  121. package/esm/util/scheduleDetachedDestroy.d.ts +18 -0
  122. package/esm/util/scheduleDetachedDestroy.js +24 -0
  123. package/esm/util/scrollLatch.d.ts +1 -0
  124. package/esm/util/scrollLatch.js +8 -0
  125. package/esm/util/stopToken.d.ts +6 -5
  126. package/esm/util/trackConfigDelta.d.ts +6 -6
  127. package/esm/util/trackConfigDelta.js +3 -3
  128. package/esm/util/types/index.d.ts +25 -20
  129. package/esm/util/usePanelVirtualScroll.d.ts +6 -2
  130. package/esm/util/usePanelVirtualScroll.js +6 -2
  131. package/esm/util/useRowVirtualScroll.d.ts +1 -1
  132. package/esm/util/useRowVirtualScroll.js +1 -1
  133. package/esm/util/useVirtualScrollWheel.js +11 -1
  134. package/esm/util/wheelZoom.d.ts +3 -1
  135. package/esm/util/wheelZoom.js +39 -14
  136. package/esm/util/withLaunchInput.d.ts +47 -2
  137. package/esm/util/withLaunchInput.js +56 -11
  138. package/package.json +10 -5
  139. package/esm/ui/promotableMenuItems.d.ts +0 -19
  140. package/esm/ui/promotableMenuItems.js +0 -91
@@ -7,6 +7,16 @@ import { createRoot } from 'react-dom/client';
7
7
  // reason to pull react/react-dom/@emotion, and the barrel is imported by
8
8
  // worker-side adapter code that never renders.
9
9
  //
10
+ // It is not part of the plugin ABI either, and that removal did break two
11
+ // published bundles before anyone noticed: react-msaview's SVG export read it
12
+ // off `@jbrowse/core/util`, and jbrowse-plugin-msaview and -tview bundle
13
+ // react-msaview. Resolved on their side -- react-msaview owns its own copy now
14
+ // (packages/lib/src/renderToStaticMarkup.ts there) -- because a rendering
15
+ // library asking its host for a renderer was the coupling worth removing, and
16
+ // because react-dom is a host external for a plugin anyway, so it costs them a
17
+ // few hundred bytes rather than a duplicate react. sharedModules.purity.test.ts
18
+ // is what keeps this file off the worker's graph now.
19
+ //
10
20
  // https://react.dev/reference/react-dom/server/renderToString#removing-rendertostring-from-the-client-code
11
21
  export function renderToStaticMarkup(node) {
12
22
  const div = document.createElement('div');
@@ -0,0 +1,18 @@
1
+ import type { IAnyStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ /**
3
+ * Free a tree an action has already let go of — a superseded session, a view
4
+ * taken out of one, a superseded rootModel the React host has detached.
5
+ *
6
+ * A task rather than a microtask: the wait is for React to unmount the
7
+ * components rendering it, which happens on the re-render that the action's own
8
+ * reactions schedule. Deferring is acceptable here, and is not elsewhere,
9
+ * because the node stays alive and detached until it fires — a late read gets a
10
+ * live tree nobody is rendering. ADR-069.
11
+ *
12
+ * The destroy itself is not optional. `beforeDestroy` is a plugin-facing
13
+ * contract (jbrowse-plugin-apollo aborts its in-flight fetches there) and core's
14
+ * `BaseTrackModel` releases the `rpcSessionId` claim that lets
15
+ * `CoreFreeResources` evict a parsed adapter from the worker — so detaching and
16
+ * forgetting would trade a console warning for an unbounded leak.
17
+ */
18
+ export declare function scheduleDetachedDestroy(node: IAnyStateTreeNode): void;
@@ -0,0 +1,24 @@
1
+ import { destroy, isAlive } from '@jbrowse/mobx-state-tree';
2
+ /**
3
+ * Free a tree an action has already let go of — a superseded session, a view
4
+ * taken out of one, a superseded rootModel the React host has detached.
5
+ *
6
+ * A task rather than a microtask: the wait is for React to unmount the
7
+ * components rendering it, which happens on the re-render that the action's own
8
+ * reactions schedule. Deferring is acceptable here, and is not elsewhere,
9
+ * because the node stays alive and detached until it fires — a late read gets a
10
+ * live tree nobody is rendering. ADR-069.
11
+ *
12
+ * The destroy itself is not optional. `beforeDestroy` is a plugin-facing
13
+ * contract (jbrowse-plugin-apollo aborts its in-flight fetches there) and core's
14
+ * `BaseTrackModel` releases the `rpcSessionId` claim that lets
15
+ * `CoreFreeResources` evict a parsed adapter from the worker — so detaching and
16
+ * forgetting would trade a console warning for an unbounded leak.
17
+ */
18
+ export function scheduleDetachedDestroy(node) {
19
+ setTimeout(() => {
20
+ if (isAlive(node)) {
21
+ destroy(node);
22
+ }
23
+ }, 0);
24
+ }
@@ -1,4 +1,5 @@
1
1
  export declare function createScrollLatch(timeoutMs?: number): {
2
2
  scroll(e: WheelEvent, cur: number, delta: number, max: number, min?: number): number | null;
3
+ holds(e: WheelEvent): boolean;
3
4
  reset(): void;
4
5
  };
@@ -47,6 +47,14 @@ export function createScrollLatch(timeoutMs = LATCH_TIMEOUT_MS) {
47
47
  }
48
48
  return null;
49
49
  },
50
+ // Whether this panel already owns the gesture `e` belongs to, i.e. it has
51
+ // scrolled during a run of events this one continues. Callers ask before
52
+ // declining an event on their own rules: a gesture that has latched keeps
53
+ // every event it fires, cross-axis momentum included, which is the whole
54
+ // point of latching.
55
+ holds(e) {
56
+ return latched && e.timeStamp - lastEvent < timeoutMs;
57
+ },
50
58
  // Forget the gesture state so the next event is treated as a fresh gesture.
51
59
  // Called when the pointer leaves the panel: a gesture the browser has
52
60
  // latched to this element must not keep the page suppressed once the cursor
@@ -31,11 +31,12 @@ import type { TimeGate } from './timeGate.ts';
31
31
  * either way over a 2000x BAM cancel burst) and that measurement was sound but
32
32
  * scoped: every loop on the alignments path is already chunked by awaits at
33
33
  * region granularity, so there was nothing there for an intra-loop probe to
34
- * interrupt. The counter-example is `getLDMatrix.ts`'s O(n²) Float32Array fill —
35
- * millions of pair computations with no await anywhere, where this probe is the
36
- * only thing that can stop the work, and which that bench never exercises. Any
37
- * future attempt to delete this needs a cancel measurement on an
38
- * await-free workload (LD or multi-sample-variant), not on a pileup.
34
+ * interrupt. The counter-example is the multi-sample-variant genotype matrix
35
+ * fill (`VariantRPC/getGenotypeMatrix.ts`) — millions of cells with no await
36
+ * anywhere, where this probe is the only thing that can stop the work, and
37
+ * which that bench never exercises. Any future attempt to delete this needs a
38
+ * cancel measurement on an await-free workload, not on a pileup. LD's O(n²)
39
+ * pair fill was the original example and went with the in-browser estimator.
39
40
  *
40
41
  * ## Which path runs where
41
42
  *
@@ -23,9 +23,9 @@
23
23
  * empty delta, and `SessionTracks.updateTrackConfiguration` used to read that as
24
24
  * "the edit netted back to the base" and revert the track's working copy — which
25
25
  * undid a removal on screen 400ms after it landed. Promotable slots made that
26
- * reachable rather than rare: the promoted-default snackbar's "Override N
27
- * customized tracks" exists to unset a slot, and an admin config is free to set
28
- * one. That branch now keeps the working copy; see the comment there.
26
+ * reachable rather than rare: a slider row's reset unsets its slot, and an
27
+ * admin config is free to set one. That branch now keeps the working copy; see
28
+ * the comment there.
29
29
  *
30
30
  * `displays` is merged by `displayId` so an edit to one display doesn't pin the
31
31
  * others. Nested config objects (e.g. `adapter`) recurse. Any other array (value
@@ -59,9 +59,9 @@ export declare function mergeTrackConfig(base: Record<string, unknown>, delta: R
59
59
  export interface TrackConfigChange {
60
60
  /**
61
61
  * Where the setting lives, and — for a row a UI can revert — its **address**:
62
- * `resetPreferenceChange` parses a promoted display-type default's path back
63
- * into the type and slot it clears. So a producer that wants a friendlier
64
- * heading sets `label` and leaves this alone.
62
+ * product-core's `resetDisplayDefault` parses a promoted display-type
63
+ * default's path back into the type and slot it clears. So a producer that
64
+ * wants a friendlier heading sets `label` and leaves this alone.
65
65
  */
66
66
  path: string[];
67
67
  from: Json;
@@ -23,9 +23,9 @@
23
23
  * empty delta, and `SessionTracks.updateTrackConfiguration` used to read that as
24
24
  * "the edit netted back to the base" and revert the track's working copy — which
25
25
  * undid a removal on screen 400ms after it landed. Promotable slots made that
26
- * reachable rather than rare: the promoted-default snackbar's "Override N
27
- * customized tracks" exists to unset a slot, and an admin config is free to set
28
- * one. That branch now keeps the working copy; see the comment there.
26
+ * reachable rather than rare: a slider row's reset unsets its slot, and an
27
+ * admin config is free to set one. That branch now keeps the working copy; see
28
+ * the comment there.
29
29
  *
30
30
  * `displays` is merged by `displayId` so an edit to one display doesn't pin the
31
31
  * others. Nested config objects (e.g. `adapter`) recurse. Any other array (value
@@ -376,22 +376,30 @@ export interface SessionWithSessionPlugins extends AbstractSessionModel {
376
376
  removeSessionPlugin: (plugin: PluginDefinition) => void;
377
377
  }
378
378
  export declare function isSessionWithSessionPlugins(thing: unknown): thing is SessionWithSessionPlugins;
379
+ export interface PermanentPluginsSafeMode {
380
+ reason: 'requested' | 'previousLaunchFailed';
381
+ /** what was loading when the launch that failed did not finish */
382
+ suspects: string[];
383
+ }
379
384
  /**
380
385
  * A session whose product keeps a plugin list of its own, outside both the
381
386
  * config and the session — jbrowse-web's permanent plugins, stored in the
382
387
  * browser against the config being viewed, so an install survives a new session
383
388
  * and the next visit.
384
389
  *
385
- * The list is not MST state, so `permanentPlugins` re-reads storage on each
386
- * call and nothing observes it. That is enough for its two readers, both of
387
- * which run in response to a click: adding or removing one asks for the whole-
388
- * app reload every plugin change needs anyway.
390
+ * The plugin store is the whole surface: the pin beside an installed plugin
391
+ * writes the list, and an entry the load skipped is a row there with a switch
392
+ * on it, which is what makes the safe mode below something a user can act on.
389
393
  */
390
394
  export interface SessionWithPermanentPlugins extends AbstractSessionModel {
391
- permanentPlugins: PluginDefinition[];
392
- permanentPluginsSkipped: boolean;
395
+ permanentPlugins: (PluginDefinition & {
396
+ disabled?: boolean;
397
+ })[];
398
+ permanentPluginsSafeMode: PermanentPluginsSafeMode | undefined;
393
399
  addPermanentPlugin: (plugin: PluginDefinition) => void;
394
400
  removePermanentPlugin: (plugin: PluginDefinition) => void;
401
+ setPermanentPluginDisabled: (plugin: PluginDefinition, disabled: boolean) => void;
402
+ reloadWithPermanentPlugins: () => void;
395
403
  }
396
404
  export declare function isSessionWithPermanentPlugins(thing: unknown): thing is SessionWithPermanentPlugins;
397
405
  /** abstract interface for a session that manages a global selection */
@@ -480,22 +488,19 @@ export interface AbstractViewModel {
480
488
  */
481
489
  trackContainers?: TrackContainer[];
482
490
  /**
483
- * every track open on this view itself — its own `tracks` and any containers
484
- * it owns instead; tracks on nested views are excluded. Derived by
485
- * `BaseViewModel`, so a view composing the base answers correctly without
486
- * implementing anything. Required, like `bodyMounted`: a stand-in that
487
- * forgets it would silently under-report instead of failing the build.
491
+ * what this view contributes to the census of what is open: the tracks it
492
+ * holds itself, and the views nested directly inside it. `BaseViewModel`
493
+ * answers both with nothing, so a view declares what it owns instead of the
494
+ * base reading properties whose meaning belongs to the view — see
495
+ * `util/openViews.ts`, where the recursion over them lives.
496
+ *
497
+ * Required, like `bodyMounted`, so a stand-in that means to be enumerated
498
+ * says so; the empty default means forgetting under-reports rather than
499
+ * putting something that is not a track in front of the readiness marker.
488
500
  */
489
501
  ownTracks: AbstractTrackModel[];
490
- /**
491
- * this view and every view nested inside it, to any depth. The walk over
492
- * `tracks` / `trackContainers` / `views` is written once, in `BaseViewModel`;
493
- * anything needing "everything on screen" asks this and `ownTracks` rather
494
- * than knowing which property a container keeps its children on.
495
- */
496
- allViews: AbstractViewModel[];
497
- /** every track open on this view or any view nested inside it */
498
- allTracks: AbstractTrackModel[];
502
+ /** the views nested directly inside this one — see `ownTracks` */
503
+ ownViews: AbstractViewModel[];
499
504
  }
500
505
  export declare function isViewModel(thing: unknown): thing is AbstractViewModel;
501
506
  type Display = {
@@ -8,11 +8,15 @@ interface PanelScrollTarget {
8
8
  * the canvas basic display: a plain wheel scrolls it, `shift` keeps scrolling it
9
9
  * once `scrollZoom` has claimed the plain wheel for the view, and ctrl/meta is
10
10
  * left to the browser and the view's pinch-zoom. Nothing here resizes anything.
11
+ * A sideways-dominant wheel is the view's pan and is declined
12
+ * (`useVirtualScrollWheel`, which says what latching does with that).
11
13
  *
12
14
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
13
15
  * DOM overflow container, so nothing scrolls them without this. The latch (inside
14
- * `applyScroll`) owns `preventDefault` but never `stopPropagation`, so a diagonal
15
- * wheel still bubbles its horizontal component to the LGV for panning.
16
+ * `applyScroll`) owns `preventDefault` but never `stopPropagation`: the event
17
+ * still reaches the LGV, which reads that flag and declines a gesture this panel
18
+ * has claimed, so a diagonal wheel scrolls the reads without also panning the
19
+ * genome sideways.
16
20
  *
17
21
  * ADR-027 keeps wheel-*intent* dispatch per handler, because the panels do not
18
22
  * agree on what a gesture means. This is the second of the two rules that
@@ -4,11 +4,15 @@ import { useVirtualScrollWheel } from './useVirtualScrollWheel.js';
4
4
  * the canvas basic display: a plain wheel scrolls it, `shift` keeps scrolling it
5
5
  * once `scrollZoom` has claimed the plain wheel for the view, and ctrl/meta is
6
6
  * left to the browser and the view's pinch-zoom. Nothing here resizes anything.
7
+ * A sideways-dominant wheel is the view's pan and is declined
8
+ * (`useVirtualScrollWheel`, which says what latching does with that).
7
9
  *
8
10
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
9
11
  * DOM overflow container, so nothing scrolls them without this. The latch (inside
10
- * `applyScroll`) owns `preventDefault` but never `stopPropagation`, so a diagonal
11
- * wheel still bubbles its horizontal component to the LGV for panning.
12
+ * `applyScroll`) owns `preventDefault` but never `stopPropagation`: the event
13
+ * still reaches the LGV, which reads that flag and declines a gesture this panel
14
+ * has claimed, so a diagonal wheel scrolls the reads without also panning the
15
+ * genome sideways.
12
16
  *
13
17
  * ADR-027 keeps wheel-*intent* dispatch per handler, because the panels do not
14
18
  * agree on what a gesture means. This is the second of the two rules that
@@ -10,7 +10,7 @@ interface RowScrollTarget {
10
10
  * Wheel gestures over a **row-stack** panel — the multi-sample variant displays
11
11
  * and MAF: `shift`+wheel resizes the rows (a vertical-zoom that keeps the row
12
12
  * under the cursor put), a plain wheel scrolls them, and anything the panel
13
- * can't consume is left to bubble to the view.
13
+ * can't consume — a sideways swipe included — is left to bubble to the view.
14
14
  *
15
15
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
16
16
  * DOM overflow container, so nothing scrolls them without this.
@@ -4,7 +4,7 @@ import { useVirtualScrollWheel } from './useVirtualScrollWheel.js';
4
4
  * Wheel gestures over a **row-stack** panel — the multi-sample variant displays
5
5
  * and MAF: `shift`+wheel resizes the rows (a vertical-zoom that keeps the row
6
6
  * under the cursor put), a plain wheel scrolls them, and anything the panel
7
- * can't consume is left to bubble to the view.
7
+ * can't consume — a sideways swipe included — is left to bubble to the view.
8
8
  *
9
9
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
10
10
  * DOM overflow container, so nothing scrolls them without this.
@@ -58,7 +58,17 @@ onWheel) {
58
58
  // not-yet-committed model read.
59
59
  const runningRef = useRef(0);
60
60
  const applyScroll = useCallback((e, { scrollTop, viewportHeight, scrollableHeight }, commit) => {
61
- if (scrollableHeight <= 0) {
61
+ // Each handler takes the gesture whose dominant axis is its own: a
62
+ // sideways swipe belongs to the view's pan, and consuming its small
63
+ // vertical component would both drift the panel and — since consuming
64
+ // means preventDefault, which is how the view knows to stay out — kill
65
+ // the pan the user actually asked for. A tie is vertical, matching the
66
+ // view's own zoom-vs-pan tie-break. Raw deltas, because both axes of one
67
+ // event share a deltaMode. `holds` is what keeps this from cutting a
68
+ // gesture in half: once the panel has latched, the sideways momentum
69
+ // trailing a vertical scroll stays with the panel.
70
+ const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
71
+ if (scrollableHeight <= 0 || (sideways && !latch.holds(e))) {
62
72
  return;
63
73
  }
64
74
  // A fresh frame (nothing pending) re-syncs to the model's live scrollTop;
@@ -17,6 +17,7 @@ export interface WheelZoomTarget {
17
17
  views: WheelZoomView[];
18
18
  scrollZoom: boolean;
19
19
  originElement: () => HTMLElement | undefined;
20
+ hold?: <T>(fn: () => T) => T;
20
21
  }
21
22
  export interface WheelZoomControllerOptions {
22
23
  element: HTMLElement;
@@ -37,9 +38,10 @@ export interface WheelZoomControllerOptions {
37
38
  /**
38
39
  * The one wheel gesture handler for linear views. Its decision matrix:
39
40
  *
41
+ * - a wheel a nested scroller already consumed → declined, it owns the gesture
40
42
  * - ctrl/meta+wheel, or (scrollZoom && |dy| >= |dx|) → zoom about the cursor
41
43
  * - shift+wheel while scrollZoom is on → declined, we pass
42
- * - a horizontal delta, unless one arrives mid-zoom → pan by dx
44
+ * - a sideways delta, unless one arrives mid-zoom → pan by dx
43
45
  *
44
46
  * Declining shift is what leaves a nested panel its own scroll
45
47
  * (`usePanelVirtualScroll`) — it is not a way back to the page. Browsers turn
@@ -89,9 +89,10 @@ export function applyZoomAccum(bpPerPx, zoomAccum, elapsedMs) {
89
89
  /**
90
90
  * The one wheel gesture handler for linear views. Its decision matrix:
91
91
  *
92
+ * - a wheel a nested scroller already consumed → declined, it owns the gesture
92
93
  * - ctrl/meta+wheel, or (scrollZoom && |dy| >= |dx|) → zoom about the cursor
93
94
  * - shift+wheel while scrollZoom is on → declined, we pass
94
- * - a horizontal delta, unless one arrives mid-zoom → pan by dx
95
+ * - a sideways delta, unless one arrives mid-zoom → pan by dx
95
96
  *
96
97
  * Declining shift is what leaves a nested panel its own scroll
97
98
  * (`usePanelVirtualScroll`) — it is not a way back to the page. Browsers turn
@@ -141,26 +142,41 @@ export function createWheelZoomController({ element, resolveTarget, swallowUnhan
141
142
  s.originLeft = origin.getBoundingClientRect().left;
142
143
  }
143
144
  const originLeft = s.originLeft;
144
- transaction(() => {
145
- if (s.zoomAccum !== 0 && originLeft !== undefined) {
146
- const offset = s.lastClientX - originLeft;
147
- for (const view of target.views) {
148
- view.zoomTo(applyZoomAccum(view.bpPerPx, s.zoomAccum, elapsed), offset);
145
+ const apply = () => {
146
+ transaction(() => {
147
+ if (s.zoomAccum !== 0 && originLeft !== undefined) {
148
+ const offset = s.lastClientX - originLeft;
149
+ for (const view of target.views) {
150
+ view.zoomTo(applyZoomAccum(view.bpPerPx, s.zoomAccum, elapsed), offset);
151
+ }
149
152
  }
150
- }
151
- if (s.scrollDelta !== 0) {
152
- for (const view of target.views) {
153
- view.horizontalScroll(s.scrollDelta);
153
+ if (s.scrollDelta !== 0) {
154
+ for (const view of target.views) {
155
+ view.horizontalScroll(s.scrollDelta);
156
+ }
154
157
  }
155
- }
156
- });
158
+ });
159
+ };
160
+ if (target.hold) {
161
+ target.hold(apply);
162
+ }
163
+ else {
164
+ apply();
165
+ }
157
166
  }
158
167
  s.zoomAccum = 0;
159
168
  s.scrollDelta = 0;
160
169
  }
161
170
  function onWheel(event) {
162
171
  onEvent?.(event);
163
- const gated = presence !== undefined && !presence.isOver;
172
+ // A nested vertical scroller preventDefaults exactly what it consumed, and
173
+ // that flag is still readable on the way up, so the view declines what a
174
+ // panel is already scrolling: a trackpad emits a small deltaX alongside
175
+ // every vertical swipe, which used to pan the genome sideways under someone
176
+ // who was only scrolling reads. No timer of our own — the scroll latch
177
+ // holds the whole continuous gesture, boundary included, and releasing it
178
+ // is the pause that ends the gesture.
179
+ const gated = event.defaultPrevented || (presence !== undefined && !presence.isOver);
164
180
  const target = gated ? undefined : resolveTarget(event);
165
181
  if (target && !(event.shiftKey && target.scrollZoom)) {
166
182
  // a gap longer than the frame-elapsed clamp means this event starts a new
@@ -216,7 +232,16 @@ export function createWheelZoomController({ element, resolveTarget, swallowUnhan
216
232
  Math.abs(deltaX) > Math.abs(2 * deltaY)) {
217
233
  event.preventDefault();
218
234
  }
219
- s.scrollDelta = accumulateScroll(s.scrollDelta, deltaX);
235
+ // Pan on a gesture that is actually sideways. A trackpad puts a small
236
+ // deltaX on every vertical swipe, and panning on it drifted the genome
237
+ // under anyone scrolling a track — including where no panel handler
238
+ // reports the scroll for us to decline, as with the natively-scrolling
239
+ // pinned-tracks block. Ties go vertical, the same way the zoom test
240
+ // above resolves them. With scrollZoom on this changes nothing: a
241
+ // vertical-dominant wheel was already taken as a zoom.
242
+ if (Math.abs(deltaX) > Math.abs(deltaY)) {
243
+ s.scrollDelta = accumulateScroll(s.scrollDelta, deltaX);
244
+ }
220
245
  }
221
246
  frame.schedule(flush);
222
247
  }
@@ -1,5 +1,34 @@
1
1
  export { legacyInitMessage, unknownKeysMessage } from './unknownSnapshotKeys.ts';
2
- import type { IAnyModelType, IModelType, SnapshotIn } from '@jbrowse/mobx-state-tree';
2
+ import type { IAnyModelType, IModelType, SnapshotIn, SnapshotOut } from '@jbrowse/mobx-state-tree';
3
+ /**
4
+ * The slice of a PluginManager the partition reads: which view type a
5
+ * snapshot's `type` names, and that type's state model as REGISTERED — the
6
+ * one `extendViewType` may have composed more properties onto after the
7
+ * factory wrapped its own.
8
+ */
9
+ export interface ViewTypeLookup {
10
+ viewTypes: {
11
+ has: (name: string) => boolean;
12
+ };
13
+ getViewType: (name: string) => {
14
+ isStateModelLoaded: boolean;
15
+ stateModel: IAnyModelType;
16
+ };
17
+ }
18
+ export interface LaunchInputOptions<M extends IAnyModelType> {
19
+ /** the PluginManager; without it the partition sees only the wrapped model */
20
+ registry?: ViewTypeLookup;
21
+ /**
22
+ * Whether a snapshot shows the launch applied — rows built, regions set. The
23
+ * blob persists only until then: a snapshot taken mid-load (an autosave
24
+ * firing before the launch autorun navigates) has to rebuild the view rather
25
+ * than drop it to the import form, and a materialized view has nothing left
26
+ * to rebuild. `installInitAutorun` keys its failure policy off the same line,
27
+ * read from the live node. `Partial` because a `stripDefault` property is
28
+ * absent from the snapshot when empty, whatever its declared type says.
29
+ */
30
+ materialized: (snap: Partial<SnapshotOut<M>>) => boolean;
31
+ }
3
32
  /**
4
33
  * How a launch key's value is told apart from the state property of the same
5
34
  * name. A key whose name collides with nothing is `launch`; the rest name the
@@ -93,6 +122,17 @@ export type LaunchInputModel<M extends IAnyModelType, Commands, PassThrough exte
93
122
  export declare function defineLaunchKeys<Commands>(): <const P extends readonly string[] = readonly never[]>(keys: Record<keyof Commands, LaunchKeySpec>, { passThrough }?: {
94
123
  passThrough?: P;
95
124
  }) => LaunchKeyRegistration<Commands, P[number]>;
125
+ /**
126
+ * The part of a launch spec that belongs on the snapshot: everything but the
127
+ * launch keys, and the identity and `launch` keys no spec may set. For a caller
128
+ * that applies the launch itself and builds the view from a snapshot — a
129
+ * synteny row, whose parent navigates it and opens its tracks — so what it
130
+ * hands MST is settings only, and the view's own partition sorts those into
131
+ * properties and reported typos.
132
+ */
133
+ export declare function snapshotSettings(spec: Record<string, unknown>, registration: LaunchKeyRegistration<unknown, string> | undefined): {
134
+ [k: string]: unknown;
135
+ };
96
136
  /**
97
137
  * Accept a view's settings written directly on the view object, and move the
98
138
  * ones a launcher has to resolve into the internal `launch` property.
@@ -104,6 +144,11 @@ export declare function defineLaunchKeys<Commands>(): <const P extends readonly
104
144
  * are about to be rejected. `afterAttach` — reached only by a snapshot that won
105
145
  * — reports what the partition captured.
106
146
  *
147
+ * `registry` is the PluginManager. With it the partition classifies against
148
+ * the view type as registered, `extendViewType`'s added properties included;
149
+ * without it, against the model as wrapped here. `materialized` draws the line
150
+ * past which the blob is no longer persisted; see `LaunchInputOptions`.
151
+ *
107
152
  * ORDER: MST runs preprocessors in the reverse of the order they were added, so
108
153
  * this belongs on the chain BEFORE a view's own legacy-key preprocessor, where
109
154
  * it partitions the snapshot MST finally consumes rather than a key that remap
@@ -120,4 +165,4 @@ export declare function defineLaunchKeys<Commands>(): <const P extends readonly
120
165
  * without a word, and `ViewType.stateModel` and every `.properties`
121
166
  * introspection site break with it.
122
167
  */
123
- export declare function withLaunchInput<M extends IAnyModelType, Commands, PassThrough extends string>(model: M, registration: LaunchKeyRegistration<Commands, PassThrough>): LaunchInputModel<M, Commands, PassThrough>;
168
+ export declare function withLaunchInput<M extends IAnyModelType, Commands, PassThrough extends string>(model: M, registration: LaunchKeyRegistration<Commands, PassThrough>, { registry, materialized }: LaunchInputOptions<M>): LaunchInputModel<M, Commands, PassThrough>;
@@ -41,6 +41,18 @@ export function defineLaunchKeys() {
41
41
  // is ever a setting and neither can be lifted off the snapshot.
42
42
  const IDENTITY = new Set(['id', 'type']);
43
43
  const LAUNCH = 'launch';
44
+ /**
45
+ * The part of a launch spec that belongs on the snapshot: everything but the
46
+ * launch keys, and the identity and `launch` keys no spec may set. For a caller
47
+ * that applies the launch itself and builds the view from a snapshot — a
48
+ * synteny row, whose parent navigates it and opens its tracks — so what it
49
+ * hands MST is settings only, and the view's own partition sorts those into
50
+ * properties and reported typos.
51
+ */
52
+ export function snapshotSettings(spec, registration) {
53
+ const keys = registration?.keys ?? {};
54
+ return Object.fromEntries(Object.entries(spec).filter(([key]) => !(key in keys) && !IDENTITY.has(key) && key !== LAUNCH));
55
+ }
44
56
  function isObject(value) {
45
57
  return !!value && typeof value === 'object';
46
58
  }
@@ -80,6 +92,31 @@ function splitEntries(kind, value) {
80
92
  state: built.length ? (Array.isArray(value) ? built : value) : undefined,
81
93
  };
82
94
  }
95
+ // The properties a snapshot's keys are classified against are the REGISTERED
96
+ // view type's, read at preprocess time: `extendViewType` composes onto the
97
+ // model after `withLaunchInput` wrapped it, and a set frozen at wrap time sent
98
+ // every added property to `unknown` — restored at its default, with a warning
99
+ // naming it a typo. The wrapped model's own properties are the answer when no
100
+ // registry is given, or the snapshot names a type it does not hold, which is
101
+ // how a factory-built model in a test behaves.
102
+ function knownPropsResolver(model, registry) {
103
+ let cachedProps;
104
+ let cachedKeys = new Set();
105
+ return (snap) => {
106
+ const name = snap.type;
107
+ const registered = registry && typeof name === 'string' && registry.viewTypes.has(name)
108
+ ? registry.getViewType(name)
109
+ : undefined;
110
+ const props = registered?.isStateModelLoaded
111
+ ? registered.stateModel.properties
112
+ : model.properties;
113
+ if (props !== cachedProps) {
114
+ cachedProps = props;
115
+ cachedKeys = new Set(Object.keys(props));
116
+ }
117
+ return cachedKeys;
118
+ };
119
+ }
83
120
  function classify(snap, { keys, passThrough }, known, out) {
84
121
  for (const [key, value] of Object.entries(snap)) {
85
122
  const spec = keys[key];
@@ -140,6 +177,11 @@ function legacyInitSnapshot(init, { keys }) {
140
177
  * are about to be rejected. `afterAttach` — reached only by a snapshot that won
141
178
  * — reports what the partition captured.
142
179
  *
180
+ * `registry` is the PluginManager. With it the partition classifies against
181
+ * the view type as registered, `extendViewType`'s added properties included;
182
+ * without it, against the model as wrapped here. `materialized` draws the line
183
+ * past which the blob is no longer persisted; see `LaunchInputOptions`.
184
+ *
143
185
  * ORDER: MST runs preprocessors in the reverse of the order they were added, so
144
186
  * this belongs on the chain BEFORE a view's own legacy-key preprocessor, where
145
187
  * it partitions the snapshot MST finally consumes rather than a key that remap
@@ -156,13 +198,14 @@ function legacyInitSnapshot(init, { keys }) {
156
198
  * without a word, and `ViewType.stateModel` and every `.properties`
157
199
  * introspection site break with it.
158
200
  */
159
- export function withLaunchInput(model, registration) {
160
- const known = new Set(Object.keys(model.properties));
201
+ export function withLaunchInput(model, registration, { registry, materialized }) {
202
+ const knownProps = knownPropsResolver(model, registry);
161
203
  const partitioned = model
162
204
  .preProcessSnapshot((snap) => {
163
205
  if (!isObject(snap)) {
164
206
  return snap;
165
207
  }
208
+ const known = knownProps(snap);
166
209
  const { init, ...rest } = snap;
167
210
  const out = {
168
211
  state: {},
@@ -207,20 +250,22 @@ export function withLaunchInput(model, registration) {
207
250
  reportMalformedRows(self, Object.keys(launch?.malformed ?? {}));
208
251
  },
209
252
  }))
210
- // `legacyInit` names the SPELLING of the snapshot this view was opened from,
211
- // and a saved one does not use it — so persisting the flag makes every later
212
- // restore report nesting the snapshot it is reading does not contain,
213
- // forever, since nothing clears it (`pendingLaunch` excludes the report
214
- // keys, so the launch autorun never runs). `unknown` and `malformed` stay:
215
- // those name content that was DISCARDED, which is still true of the saved
216
- // snapshot, and the blob is the only record of it.
253
+ // Whole while the view has yet to materialize, since the pending launch is
254
+ // then the only thing a reload can rebuild the view from, and gone once it
255
+ // has — the report keys with it, so a saved session never carries a
256
+ // typo report about content it no longer holds. `legacyInit` goes either
257
+ // way: it names the SPELLING of the snapshot this view was opened from,
258
+ // and the saved one does not use it, so persisting it would report nesting
259
+ // on every restore forever (`pendingLaunch` excludes the report keys, so
260
+ // no launch run ever clears it).
217
261
  .postProcessSnapshot((snap) => {
218
262
  const launch = snap[LAUNCH];
219
- if (!isObject(launch) || !launch.legacyInit) {
263
+ if (!isObject(launch)) {
220
264
  return snap;
221
265
  }
222
266
  const { legacyInit, ...kept } = launch;
223
- return Object.keys(kept).length
267
+ return Object.keys(kept).length &&
268
+ !materialized(snap)
224
269
  ? { ...snap, [LAUNCH]: kept }
225
270
  : Object.fromEntries(Object.entries(snap).filter(([key]) => key !== LAUNCH));
226
271
  });