@jbrowse/core 5.0.0-beta.1 → 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 (199) hide show
  1. package/README.md +112 -43
  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/SequenceFeatureDetails/test_data/NCDN.d.ts +2 -2
  8. package/esm/BaseFeatureWidget/index.d.ts +1 -1
  9. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +8 -1
  10. package/esm/BaseFeatureWidget/stateModelFactory.js +6 -0
  11. package/esm/BaseFeatureWidget/types.d.ts +10 -2
  12. package/esm/PluginLoader.d.ts +1 -0
  13. package/esm/PluginLoader.js +52 -8
  14. package/esm/PluginManager.d.ts +99 -17
  15. package/esm/PluginManager.js +194 -26
  16. package/esm/ReExports/BaseFeatureDetails.js +1 -1
  17. package/esm/ReExports/MuiDataGridReExports.d.ts +1 -0
  18. package/esm/ReExports/MuiDataGridReExports.js +2 -2
  19. package/esm/ReExports/barrelOnlyNames.d.ts +5 -0
  20. package/esm/ReExports/barrelOnlyNames.js +63 -0
  21. package/esm/ReExports/modules.js +31 -109
  22. package/esm/ReExports/publicTracks.d.ts +1 -0
  23. package/esm/ReExports/publicTracks.js +14 -0
  24. package/esm/ReExports/publicUi.d.ts +4 -0
  25. package/esm/ReExports/publicUi.js +33 -0
  26. package/esm/ReExports/publicUtil.d.ts +1 -1
  27. package/esm/ReExports/publicUtil.js +8 -4
  28. package/esm/ReExports/sharedModules.d.ts +96 -0
  29. package/esm/ReExports/sharedModules.js +66 -0
  30. package/esm/ReExports/uiStub.d.ts +2 -0
  31. package/esm/ReExports/uiStub.js +11 -0
  32. package/esm/ReExports/workerModules.d.ts +2 -0
  33. package/esm/ReExports/workerModules.js +15 -0
  34. package/esm/ReExports/workerNamespaceNames.d.ts +11 -0
  35. package/esm/ReExports/workerNamespaceNames.js +281 -0
  36. package/esm/configuration/configurationSchema.d.ts +5 -38
  37. package/esm/configuration/configurationSchema.js +64 -40
  38. package/esm/configuration/index.d.ts +2 -2
  39. package/esm/configuration/index.js +1 -1
  40. package/esm/configuration/promotableDefaults.d.ts +54 -19
  41. package/esm/configuration/promotableDefaults.js +117 -48
  42. package/esm/configuration/promotablePin.d.ts +50 -40
  43. package/esm/configuration/promotableResolve.d.ts +1 -1
  44. package/esm/configuration/promotableResolve.js +1 -1
  45. package/esm/configuration/tabixIndexFields.d.ts +2 -2
  46. package/esm/configuration/tabixIndexFields.js +2 -2
  47. package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.d.ts +12 -0
  48. package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.js +35 -4
  49. package/esm/data_adapters/BaseAdapter/featureDensity.d.ts +41 -0
  50. package/esm/data_adapters/BaseAdapter/featureDensity.js +32 -0
  51. package/esm/data_adapters/BaseAdapter/index.d.ts +2 -1
  52. package/esm/data_adapters/BaseAdapter/index.js +1 -0
  53. package/esm/data_adapters/BaseAdapter/stats.d.ts +33 -1
  54. package/esm/data_adapters/BaseAdapter/stats.js +52 -13
  55. package/esm/data_adapters/BaseAdapter/types.d.ts +3 -8
  56. package/esm/pluggableElementTypes/AdapterType.d.ts +2 -1
  57. package/esm/pluggableElementTypes/AdapterType.js +1 -1
  58. package/esm/pluggableElementTypes/DisplayType.d.ts +4 -12
  59. package/esm/pluggableElementTypes/DisplayType.js +3 -10
  60. package/esm/pluggableElementTypes/LazyStateModelElement.d.ts +27 -0
  61. package/esm/pluggableElementTypes/LazyStateModelElement.js +71 -0
  62. package/esm/pluggableElementTypes/ViewType.d.ts +20 -4
  63. package/esm/pluggableElementTypes/ViewType.js +32 -4
  64. package/esm/pluggableElementTypes/extendElementType.d.ts +1 -2
  65. package/esm/pluggableElementTypes/extendElementType.js +9 -5
  66. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +44 -3
  67. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +75 -2
  68. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +21 -0
  69. package/esm/pluggableElementTypes/models/BaseTrackModel.js +34 -2
  70. package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +49 -0
  71. package/esm/pluggableElementTypes/models/BaseViewModel.js +28 -0
  72. package/esm/pluggableElementTypes/models/assertDisplayContract.d.ts +14 -10
  73. package/esm/pluggableElementTypes/models/assertDisplayContract.js +25 -32
  74. package/esm/pluggableElementTypes/models/baseTrackConfig.js +7 -1
  75. package/esm/rpc/RpcRegistry.d.ts +10 -0
  76. package/esm/rpc/byteBudget.d.ts +9 -0
  77. package/esm/rpc/coreRpcMethods.d.ts +1 -0
  78. package/esm/rpc/coreRpcMethods.js +1 -0
  79. package/esm/rpc/methods/CoreGetFeatureDensity.d.ts +11 -0
  80. package/esm/rpc/methods/CoreGetFeatureDensity.js +22 -0
  81. package/esm/svg/svgReady.d.ts +1 -0
  82. package/esm/ui/BaseExportSvgDialog.d.ts +1 -1
  83. package/esm/ui/CascadingMenu.js +30 -8
  84. package/esm/ui/ErrorMessageStackTraceDialog.js +5 -6
  85. package/esm/ui/ExportSvgDialog.d.ts +1 -1
  86. package/esm/ui/LoadingOverlay.d.ts +14 -0
  87. package/esm/ui/LoadingOverlay.js +1 -1
  88. package/esm/ui/MenuTypes.d.ts +14 -5
  89. package/esm/ui/MenuTypes.js +3 -0
  90. package/esm/ui/PaletteContext.d.ts +11 -4
  91. package/esm/ui/PaletteContext.js +34 -5
  92. package/esm/ui/PinAdornment.js +31 -43
  93. package/esm/ui/ScrollChrome.d.ts +16 -0
  94. package/esm/ui/ScrollChrome.js +12 -0
  95. package/esm/ui/ScrollZoomToggle.d.ts +4 -3
  96. package/esm/ui/ScrollZoomToggle.js +4 -3
  97. package/esm/ui/SizeSliderRow.js +1 -1
  98. package/esm/ui/index.d.ts +3 -3
  99. package/esm/ui/index.js +4 -4
  100. package/esm/ui/launchViewMenu.js +7 -1
  101. package/esm/ui/legendMenuItem.d.ts +10 -16
  102. package/esm/ui/legendMenuItem.js +9 -20
  103. package/esm/ui/makeSizeMenu.js +1 -1
  104. package/esm/ui/menuItemAdornment.d.ts +2 -3
  105. package/esm/ui/menuItemAdornment.js +2 -3
  106. package/esm/ui/menuItems.d.ts +2 -3
  107. package/esm/ui/menuItems.js +1 -2
  108. package/esm/ui/palette.d.ts +11 -11
  109. package/esm/ui/palette.js +11 -11
  110. package/esm/ui/promotablePinCoverage.d.ts +16 -2
  111. package/esm/ui/promotablePinCoverage.js +40 -3
  112. package/esm/ui/theme.js +5 -0
  113. package/esm/ui/toggleMenuItems.d.ts +14 -9
  114. package/esm/ui/toggleMenuItems.js +32 -15
  115. package/esm/util/Base1DUtils.d.ts +4 -2
  116. package/esm/util/Base1DUtils.js +12 -4
  117. package/esm/util/SvgCanvas.js +4 -3
  118. package/esm/util/adapterConfigKey.d.ts +12 -0
  119. package/esm/util/adapterConfigKey.js +14 -0
  120. package/esm/util/adapterMetadata.js +2 -1
  121. package/esm/util/addAndShowTrack.d.ts +1 -1
  122. package/esm/util/addAndShowTrack.js +5 -1
  123. package/esm/util/addTrackFromWidget.js +2 -2
  124. package/esm/util/analytics.js +4 -5
  125. package/esm/util/cacheBudgets.d.ts +22 -19
  126. package/esm/util/cacheBudgets.js +22 -19
  127. package/esm/util/clampToContig.d.ts +17 -0
  128. package/esm/util/clampToContig.js +19 -0
  129. package/esm/util/colorBits.d.ts +2 -1
  130. package/esm/util/colorBits.js +2 -5
  131. package/esm/util/createStopTokenRotation.d.ts +9 -7
  132. package/esm/util/createStopTokenRotation.js +7 -27
  133. package/esm/util/featureTypes.d.ts +24 -0
  134. package/esm/util/featureTypes.js +35 -0
  135. package/esm/util/fetchContext.d.ts +2 -2
  136. package/esm/util/fetchContext.js +2 -2
  137. package/esm/util/fetchPhases.d.ts +10 -10
  138. package/esm/util/hooks.d.ts +8 -0
  139. package/esm/util/hooks.js +27 -0
  140. package/esm/util/index.d.ts +6 -5
  141. package/esm/util/index.js +5 -4
  142. package/esm/util/installFetch.d.ts +71 -34
  143. package/esm/util/installFetch.js +57 -29
  144. package/esm/util/installInitAutorun.d.ts +14 -13
  145. package/esm/util/installInitAutorun.js +17 -17
  146. package/esm/util/installPrerequisiteFetch.d.ts +86 -0
  147. package/esm/util/installPrerequisiteFetch.js +73 -0
  148. package/esm/util/lazyAfterAttach.d.ts +13 -0
  149. package/esm/util/lazyAfterAttach.js +31 -0
  150. package/esm/util/legendCandidates.d.ts +50 -0
  151. package/esm/util/legendCandidates.js +87 -0
  152. package/esm/util/mstUtils.d.ts +12 -1
  153. package/esm/util/mstUtils.js +20 -2
  154. package/esm/util/openFeatureWidget.d.ts +5 -0
  155. package/esm/util/openFeatureWidget.js +1 -0
  156. package/esm/util/openViews.d.ts +16 -0
  157. package/esm/util/openViews.js +22 -0
  158. package/esm/util/parentWalk.d.ts +1 -1
  159. package/esm/util/parentWalk.js +1 -1
  160. package/esm/util/pluginStore.d.ts +1 -0
  161. package/esm/util/pluginStore.js +8 -1
  162. package/esm/util/range.d.ts +13 -9
  163. package/esm/util/range.js +17 -8
  164. package/esm/util/renderToStaticMarkup.js +10 -0
  165. package/esm/util/rxjs.d.ts +16 -0
  166. package/esm/util/rxjs.js +49 -0
  167. package/esm/util/scheduleDetachedDestroy.d.ts +18 -0
  168. package/esm/util/scheduleDetachedDestroy.js +24 -0
  169. package/esm/util/scrollLatch.d.ts +1 -0
  170. package/esm/util/scrollLatch.js +8 -0
  171. package/esm/util/stopToken.d.ts +6 -5
  172. package/esm/util/tabix.d.ts +10 -4
  173. package/esm/util/tabix.js +11 -5
  174. package/esm/util/trackConfigDelta.d.ts +6 -6
  175. package/esm/util/trackConfigDelta.js +3 -3
  176. package/esm/util/tracks.d.ts +48 -0
  177. package/esm/util/tracks.js +192 -66
  178. package/esm/util/types/index.d.ts +58 -10
  179. package/esm/util/types/index.js +3 -0
  180. package/esm/util/unknownSnapshotKeys.d.ts +36 -0
  181. package/esm/util/unknownSnapshotKeys.js +69 -0
  182. package/esm/util/usePanelVirtualScroll.d.ts +6 -2
  183. package/esm/util/usePanelVirtualScroll.js +6 -2
  184. package/esm/util/useRowVirtualScroll.d.ts +1 -1
  185. package/esm/util/useRowVirtualScroll.js +1 -1
  186. package/esm/util/useVirtualScrollWheel.js +11 -1
  187. package/esm/util/viewStatus.d.ts +23 -0
  188. package/esm/util/viewStatus.js +17 -0
  189. package/esm/util/wheelZoom.d.ts +3 -1
  190. package/esm/util/wheelZoom.js +39 -14
  191. package/esm/util/whenViewSettled.d.ts +17 -4
  192. package/esm/util/whenViewSettled.js +19 -6
  193. package/esm/util/withLaunchInput.d.ts +168 -0
  194. package/esm/util/withLaunchInput.js +273 -0
  195. package/package.json +19 -11
  196. package/esm/ui/promotableMenuItems.d.ts +0 -19
  197. package/esm/ui/promotableMenuItems.js +0 -89
  198. package/esm/util/applyInitSettings.d.ts +0 -79
  199. package/esm/util/applyInitSettings.js +0 -91
@@ -33,7 +33,7 @@ export async function runFetchOnce(self, active, args, { run, commit, setError,
33
33
  const { stopToken, isCurrent, statusCallback, end } = active;
34
34
  // synchronous, so it lands while this run is by construction the current one
35
35
  setError(undefined);
36
- onBegin?.(active);
36
+ onBegin?.(stopToken);
37
37
  try {
38
38
  const result = await run(args, makeFetchContext(self, {
39
39
  stopToken,
@@ -65,7 +65,7 @@ export async function runFetchOnce(self, active, args, { run, commit, setError,
65
65
  }
66
66
  }
67
67
  /**
68
- * The two dev-only checks every fetch installer owes, together because they are
68
+ * The two contract checks every fetch installer owes, together because they are
69
69
  * the same doctrine: one reports a foundation attached twice, the other reports
70
70
  * a `reload()` the gate still declines — the dead Retry button.
71
71
  */
@@ -73,38 +73,62 @@ function installContractChecks(self, contract) {
73
73
  assertDisplayContract(self, contract);
74
74
  return makeRetryContractCheck(self);
75
75
  }
76
+ /**
77
+ * The rotation for an installation that was lent none: created here, so
78
+ * disposed here — a lent one is the host's, and resetting its status window
79
+ * would drop a write the host's other operations are still owed.
80
+ */
81
+ function ownRotation(self, report) {
82
+ const rotation = createStopTokenRotation(self, report);
83
+ addDisposer(self, () => {
84
+ rotation.dispose();
85
+ });
86
+ return rotation;
87
+ }
76
88
  /**
77
89
  * A whole fetch, installed: the rotation, the leading-edge autorun, the
78
90
  * unconditional trigger reads, the gates, and {@link runFetchOnce} under them.
79
91
  *
80
- * Every fetch in the tree is this shape except one — the prerequisite reads
81
- * (HiC's header, the multi-sample sample list), both comparative displays, the
82
- * circular view's chord fetch, the breakpoint overlay fetch and the multi-way
83
- * synteny display's two DEPENDENT fetches (lane genes, lane links, each gated on
84
- * the key its own commit stamps) all take it. The
85
- * exception is `FetchMixin.runFetch`, the viewport fetch of an LGV display: its
86
- * rotation is a member rather than a closure (so `cancelFetch` can reach it)
87
- * and its trigger list is the per-region or global foundation's, so it holds
88
- * `runFetchOnce` directly and the autorun above it is that foundation's.
92
+ * Every fetch in the tree is this shape except one family — the prerequisite
93
+ * reads (HiC's header, the multi-sample sample list), the two keyed families
94
+ * (`installGlobalFetchAutorun` and `installComparativeFetchAutorun`, which lend
95
+ * `FetchMixin`'s rotation through the `rotation` option so `cancelFetch` and
96
+ * the Cancel button reach the fetch they install), the circular view's chord
97
+ * fetch, the breakpoint overlay fetch and the multi-way synteny display's two
98
+ * DEPENDENT fetches (lane genes, lane links, each gated on the key its own
99
+ * commit stamps) all take it. The
100
+ * exception is the per-region family's `FetchMixin.runFetch`: its trigger is
101
+ * `planRegionFetch`'s autorun and its commits stream per region through
102
+ * `ctx.commitRegion`, so it holds `runFetchOnce` directly and the autorun above
103
+ * it is that foundation's.
89
104
  *
90
105
  * What a caller supplies is exactly what differs between them: which reads wake
91
106
  * it (`prepare`, plus `gate`), what a round trip commits, where the loading
92
107
  * flag lives (`onBegin` / `onEnd`) and where the status goes (`report`).
93
- *
94
- * Returns the rotation's `cancel`, for a family whose model owns the user-facing
95
- * Cancel button: the flag alone is not a cancel — nothing rotates the token, the
96
- * stopped RPC stays current, and it commits over the load the user just stopped.
97
108
  */
98
- export function installFetch(self, { name, delay, report, gate, contract, fetchKey, committedKey, prepare, run, commit, setError, onBegin, onEnd, }) {
109
+ export function installFetch(self, opts) {
110
+ const { name, delay, gate, contract, fetchKey, loadedKey, heldAnswers, prepare, run, commit, setError, onBegin, onEnd, } = opts;
99
111
  const noteFetchAutorunRun = contract === undefined ? undefined : installContractChecks(self, contract);
100
- const rotation = createStopTokenRotation(self, report);
112
+ const lent = opts.rotation;
113
+ const rotation = lent === undefined ? ownRotation(self, opts.report) : lent;
101
114
  // The `reloadCounter` this installation last issued a fetch at. Per
102
115
  // installation and per instance, so a host with several fetches gives each its
103
116
  // own — and a closure rather than model state, because nothing outside reads
104
117
  // it and a volatile would be one more thing a host has to declare.
105
118
  let issuedEpoch;
106
- const stamp = committedKey ? undefined : observable.box();
107
- const heldKey = committedKey ?? (() => stamp.get());
119
+ // The skeleton's own stamp exists only where there is a key to stamp — a
120
+ // fetch with no `fetchKey` never reads it, so allocating one would only feed
121
+ // it `undefined` on every commit.
122
+ const stamp = loadedKey === undefined && fetchKey !== undefined
123
+ ? observable.box()
124
+ : undefined;
125
+ const heldKey = loadedKey ?? (() => stamp?.get());
126
+ // the freshness gate in whichever form the caller declared it
127
+ const held = heldAnswers !== undefined
128
+ ? heldAnswers
129
+ : fetchKey !== undefined
130
+ ? (args) => isDataCurrent(heldKey(), fetchKey(args))
131
+ : undefined;
108
132
  const commitAndStamp = stamp
109
133
  ? (result, args) => {
110
134
  runInAction(() => {
@@ -113,9 +137,6 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
113
137
  });
114
138
  }
115
139
  : commit;
116
- addDisposer(self, () => {
117
- rotation.dispose();
118
- });
119
140
  leadingEdgeAutorun(self,
120
141
  // #region voidTracking
121
142
  () => {
@@ -128,13 +149,23 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
128
149
  // Cancel would be a one-way door with a Retry button on it. Order, not
129
150
  // just position: counter first, then this, then the gates.
130
151
  const canceled = self.fetchCanceled === true;
152
+ // Above the gate, not only above `prepare`: teardown mutates the
153
+ // observables this body reads before the disposers run, and every `gate`
154
+ // but the breakpoint view's reaches the containing view or track through a
155
+ // parent walk (`host.initialized`, `isMinimized`, `getContainingView`),
156
+ // which throws once the node has left the tree. This catches the destroyed
157
+ // node; the detached-and-still-alive window (ADR-069) is covered only by
158
+ // the walk's own cache. Nothing is reported for a dead node — there is no
159
+ // Retry button left to be dead, and the check would read three more
160
+ // members of a corpse to decide it.
161
+ if (!isAlive(self)) {
162
+ return false;
163
+ }
131
164
  if (canceled || gate?.() === false) {
132
165
  noteFetchAutorunRun?.('gated');
133
166
  return false;
134
167
  }
135
- // Teardown mutates observables `prepare` reads before the disposers run,
136
- // and getContainingView on a detached node warns then throws.
137
- const args = isAlive(self) ? prepare() : undefined;
168
+ const args = prepare();
138
169
  if (args === undefined) {
139
170
  noteFetchAutorunRun?.('declined');
140
171
  return false;
@@ -145,9 +176,7 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
145
176
  // consuming the retry here costs nothing — while a reload landing
146
177
  // mid-flight is answered by the re-run the counter read above already
147
178
  // guarantees.
148
- if (fetchKey !== undefined &&
149
- isDataCurrent(heldKey(), fetchKey(args)) &&
150
- reloadEpoch === issuedEpoch) {
179
+ if (held !== undefined && held(args) && reloadEpoch === issuedEpoch) {
151
180
  noteFetchAutorunRun?.('declined');
152
181
  return false;
153
182
  }
@@ -174,5 +203,4 @@ export function installFetch(self, { name, delay, report, gate, contract, fetchK
174
203
  },
175
204
  // #endregion
176
205
  { name, delay });
177
- return rotation.cancel;
178
206
  }
@@ -1,13 +1,13 @@
1
1
  import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
2
2
  interface InitAutorunHost<T> extends IStateTreeNode {
3
- init?: T;
4
- setInit: (init?: T) => void;
3
+ pendingLaunch?: T;
4
+ setLaunch: (launch?: T) => void;
5
5
  setError: (error: unknown) => void;
6
6
  }
7
7
  export interface InitApplyContext {
8
8
  /**
9
9
  * True once this init can no longer be the one being applied: the node is
10
- * gone, or a newer `setInit` has replaced it. Any mid-apply wait that can
10
+ * gone, or a newer `setLaunch` has replaced it. Any mid-apply wait that can
11
11
  * park indefinitely must fold this into its condition — an unbounded wait on
12
12
  * state that never arrives holds the drain open, and the newer init that
13
13
  * would have superseded it is exactly what gets stranded. This is the causal
@@ -26,35 +26,36 @@ interface InitAutorunOptions<T> {
26
26
  */
27
27
  ready: () => boolean;
28
28
  /**
29
- * Whether the view has materialized — the same line each view's
30
- * postProcessSnapshot draws for persistence (dotplot: assemblyNames set;
31
- * synteny: rows built). Decides the failure policy, see below.
29
+ * Whether the view has materialized — the line `withLaunchInput`'s
30
+ * `materialized` draws for persistence, read from the live node (dotplot:
31
+ * assemblyNames set; synteny: rows built). Decides the failure policy, see
32
+ * below.
32
33
  */
33
34
  materialized: () => boolean;
34
35
  /** Apply one init blob. Owns the ordered steps; never clears `init`. */
35
36
  apply: (init: T, ctx: InitApplyContext) => Promise<void>;
36
37
  }
37
38
  /**
38
- * The init state machine shared by the views that take a declarative `init`
39
+ * The launch state machine shared by the views that take a declarative launch
39
40
  * blob (LinearGenomeView, DotplotView, LinearSyntenyView): the re-entry guard,
40
41
  * the readiness gate, the serialized drain, the identity-checked clear, the
41
42
  * isAlive guards, and one failure policy.
42
43
  *
43
- * The failure policy keys off `materialized`, the same line each view's
44
- * postProcessSnapshot already draws for persistence:
44
+ * The failure policy keys off `materialized`, the same line `withLaunchInput`
45
+ * draws for persistence:
45
46
  *
46
- * - Not materialized — nothing is on screen and `init` is still persisted, so
47
+ * - Not materialized — nothing is on screen and `launch` is still persisted, so
47
48
  * keep it for a reload retry and `setError`. That flips the view onto its
48
49
  * import form, which renders the error in its own banner; a snackbar would
49
50
  * state the same failure twice and the banner is the one that persists.
50
51
  * - Materialized — the view works, so a later failure is one sub-step's
51
52
  * problem and must not take the view down with it. `setError` would, since
52
53
  * showImportForm keys off `error` alone and would discard rows that loaded
53
- * fine, so report it as a snackbar instead. Clearing `init` is also what
54
+ * fine, so report it as a snackbar instead. Clearing `launch` is also what
54
55
  * disarms the re-fire: `ready` usually folds in the measured width, so
55
- * leaving it set lets any resize re-run `apply` over a half-applied init.
56
+ * leaving it set lets any resize re-run `apply` over a half-applied blob.
56
57
  *
57
- * `apply` therefore never clears `init` and never catches for reporting — it
58
+ * `apply` therefore never clears `launch` and never catches for reporting — it
58
59
  * catches only the failures it wants to keep going through (a bad locstring in
59
60
  * one row), and everything it lets escape is fatal-as-of-that-point.
60
61
  */
@@ -1,19 +1,19 @@
1
1
  import { addDisposer, isAlive } from '@jbrowse/mobx-state-tree';
2
2
  import { autorun } from 'mobx';
3
3
  import { getNotificationSink } from './sessionServices.js';
4
- // Clear only when `init` is still the exact object we applied. A setInit that
5
- // lands while `apply` is awaiting (React StrictMode remounts, a programmatic
6
- // re-launch) swaps in a new object; clearing unconditionally would silently
7
- // drop that pending init. Leaving it lets the drain loop apply it next.
4
+ // Clear only when the pending blob is still the exact object we applied. A
5
+ // setLaunch that lands while `apply` is awaiting (React StrictMode remounts, a
6
+ // programmatic re-launch) swaps in a new object; clearing unconditionally would
7
+ // silently drop it. Leaving it lets the drain loop apply it next.
8
8
  function clearIfUnchanged(self, init) {
9
- if (isAlive(self) && self.init === init) {
10
- self.setInit(undefined);
9
+ if (isAlive(self) && self.pendingLaunch === init) {
10
+ self.setLaunch(undefined);
11
11
  }
12
12
  }
13
13
  async function applyInitOnce(self, init, { apply, materialized }) {
14
14
  try {
15
15
  await apply(init, {
16
- superseded: () => !isAlive(self) || self.init !== init,
16
+ superseded: () => !isAlive(self) || self.pendingLaunch !== init,
17
17
  });
18
18
  clearIfUnchanged(self, init);
19
19
  }
@@ -34,7 +34,7 @@ async function applyInitOnce(self, init, { apply, materialized }) {
34
34
  }
35
35
  }
36
36
  function currentInit(self, { ready }) {
37
- return isAlive(self) && ready() ? self.init : undefined;
37
+ return isAlive(self) && ready() ? self.pendingLaunch : undefined;
38
38
  }
39
39
  // Apply pending inits one at a time until none remain. Serializing (rather
40
40
  // than letting the autorun run `apply` concurrently) is what keeps re-entrant
@@ -47,32 +47,32 @@ async function drainInit(self, opts) {
47
47
  while (pending !== undefined) {
48
48
  await applyInitOnce(self, pending, opts);
49
49
  const next = currentInit(self, opts);
50
- // a fatal failure keeps `init` for a reload retry; the same object still
50
+ // a fatal failure keeps `launch` for a reload retry; the same object still
51
51
  // sitting there means it was not consumed, so stop rather than spin on it
52
52
  pending = next === pending ? undefined : next;
53
53
  }
54
54
  }
55
55
  /**
56
- * The init state machine shared by the views that take a declarative `init`
56
+ * The launch state machine shared by the views that take a declarative launch
57
57
  * blob (LinearGenomeView, DotplotView, LinearSyntenyView): the re-entry guard,
58
58
  * the readiness gate, the serialized drain, the identity-checked clear, the
59
59
  * isAlive guards, and one failure policy.
60
60
  *
61
- * The failure policy keys off `materialized`, the same line each view's
62
- * postProcessSnapshot already draws for persistence:
61
+ * The failure policy keys off `materialized`, the same line `withLaunchInput`
62
+ * draws for persistence:
63
63
  *
64
- * - Not materialized — nothing is on screen and `init` is still persisted, so
64
+ * - Not materialized — nothing is on screen and `launch` is still persisted, so
65
65
  * keep it for a reload retry and `setError`. That flips the view onto its
66
66
  * import form, which renders the error in its own banner; a snackbar would
67
67
  * state the same failure twice and the banner is the one that persists.
68
68
  * - Materialized — the view works, so a later failure is one sub-step's
69
69
  * problem and must not take the view down with it. `setError` would, since
70
70
  * showImportForm keys off `error` alone and would discard rows that loaded
71
- * fine, so report it as a snackbar instead. Clearing `init` is also what
71
+ * fine, so report it as a snackbar instead. Clearing `launch` is also what
72
72
  * disarms the re-fire: `ready` usually folds in the measured width, so
73
- * leaving it set lets any resize re-run `apply` over a half-applied init.
73
+ * leaving it set lets any resize re-run `apply` over a half-applied blob.
74
74
  *
75
- * `apply` therefore never clears `init` and never catches for reporting — it
75
+ * `apply` therefore never clears `launch` and never catches for reporting — it
76
76
  * catches only the failures it wants to keep going through (a bad locstring in
77
77
  * one row), and everything it lets escape is fatal-as-of-that-point.
78
78
  */
@@ -93,7 +93,7 @@ export function installInitAutorun(self, opts) {
93
93
  // reading a detached node throws — here into an unawaited promise.
94
94
  const alive = isAlive(self);
95
95
  const isReady = alive && opts.ready();
96
- const init = alive ? self.init : undefined;
96
+ const init = alive ? self.pendingLaunch : undefined;
97
97
  if (isReady && init !== undefined && !draining) {
98
98
  draining = true;
99
99
  try {
@@ -0,0 +1,86 @@
1
+ import type { StatusReporter } from './createStopTokenRotation.ts';
2
+ import type { FetchContext } from './fetchContext.ts';
3
+ import type { FetchSkeletonHost } from './installFetch.ts';
4
+ /**
5
+ * What a prerequisite read needs of the display it runs on, beside the
6
+ * skeleton's own three. Duck-typed rather than a foundation's `Instance` type,
7
+ * because the hosts compose different foundations — one per fetch family, and
8
+ * the comparative displays compose none of them — and this is the only thing
9
+ * they have in common.
10
+ */
11
+ export interface PrerequisiteFetchHost extends FetchSkeletonHost {
12
+ /**
13
+ * Read in `prepare`, so it is **tracked**: an adapter edited in the config
14
+ * editor has to re-read, and `run`'s own reads are untracked by contract.
15
+ */
16
+ adapterConfig: Record<string, unknown>;
17
+ isMinimized: boolean;
18
+ }
19
+ /**
20
+ * A **prerequisite read**: one RPC about the adapter itself, beside the
21
+ * display's primary fetch. Three in the tree — HiC's `CoreGetInfo` header read,
22
+ * whose `resolutions` every contacts fetch waits on; the multi-sample sample
23
+ * list, which `fetchNeeded` declines until; and the tiered alignment file's
24
+ * `CoreGetInfo` header, which decides the LOD tier a synteny fetch asks for —
25
+ * and they sit on three different fetch foundations, which is why this sits in
26
+ * core beside the skeleton and not beside any of them.
27
+ *
28
+ * They had the shape below hand-copied, comment for comment, and each term is a
29
+ * rule rather than a preference:
30
+ *
31
+ * - **the adapter config is the whole trigger and the whole key.** Tracked in
32
+ * `prepare` so a config-editor edit re-reads; keyed on the same value so an
33
+ * un-minimize over the same file re-reads nothing. Getting one without the
34
+ * other gives either a stale header or a walk of a `.hic` norm-vector index on
35
+ * every expand.
36
+ * - **minimized is the gate**, and a display with a second condition of its own
37
+ * passes `gate` — the sample-list read waits for the LGV to be measured as
38
+ * well, so a full-file scan does not start ahead of the display's own first
39
+ * fetch, and the tier read waits on the track declaring a tier threshold at
40
+ * all.
41
+ * - **no `contract`.** This is the second fetch on a display whose foundation
42
+ * installed both checks already: `assertDisplayContract` would report the
43
+ * double-attach it exists to catch, and a second retry check would consume the
44
+ * same `reloadCounter` bumps as the first.
45
+ * - **where the status goes is the caller's** (`report`). The two LGV reads
46
+ * lend the display's own status window, so the read takes a slot beside the
47
+ * primary fetch rather than opening a second writer over the one status field
48
+ * (ADR-081) — and it matters more here than for most fetches: a prerequisite
49
+ * runs inside the pre-first-paint window, where the scrim is up because
50
+ * nothing is drawn rather than because `isLoading` is, so `statusMessage` is
51
+ * the only thing that can say what is happening. The tier read narrates
52
+ * nothing; a header is not a load.
53
+ *
54
+ * `createAdapterMetadataFetch` keys on the same adapter config and is
55
+ * deliberately NOT one of these: no trigger, no stop token and no status,
56
+ * because it is fetched lazily when a details widget opens and the widget opens
57
+ * on the result. A prerequisite read is the one a display's own fetch waits on.
58
+ *
59
+ * **A declaration, not the skeleton this name used to be.** There was an
60
+ * `installPrerequisiteFetch` until 2026-08-23, and `3c5aa7fce2` deleted it for
61
+ * a good reason: it carried a `runOne` of its own — a fifth copy of begin →
62
+ * clear → run → commit-if-current → `handleFetchError` → end — and once
63
+ * `installFetch` owned that sequence, a second one was pure drift surface. This
64
+ * one adds no sequence, no rotation and no error rule; it is terms handed to
65
+ * `installFetch`, the same shape `installGlobalFetchAutorun` and
66
+ * `installComparativeFetchAutorun` are. Add nothing here that a family could
67
+ * not state as an option to the skeleton.
68
+ *
69
+ * Everything the skeleton gives it is the point of going through the skeleton:
70
+ * the latest-wins rotation (so a superseding read aborts the one it replaced,
71
+ * and only a superseding read does — a user cancel of the primary fetch cannot
72
+ * strand the display the way sharing its token would), the clear at the start,
73
+ * the currency-guarded error rule, the unconditional `reloadCounter` read that
74
+ * makes the chrome's Retry re-run this, and the retired status slot that keeps a
75
+ * failed read from leaving a progress chip up for good.
76
+ */
77
+ export declare function installPrerequisiteFetch<TResult>(self: PrerequisiteFetchHost, { name, delay, report, gate, run, commit, setError, }: {
78
+ name: string;
79
+ delay: number;
80
+ report: StatusReporter;
81
+ /** an extra term of the display's own, ANDed under `!isMinimized` */
82
+ gate?: () => boolean;
83
+ run: (adapterConfig: Record<string, unknown>, ctx: FetchContext) => Promise<TResult | undefined>;
84
+ commit: (result: TResult) => void;
85
+ setError: (error?: unknown) => void;
86
+ }): void;
@@ -0,0 +1,73 @@
1
+ import { adapterConfigKey } from './adapterConfigKey.js';
2
+ import { installFetch } from './installFetch.js';
3
+ /**
4
+ * A **prerequisite read**: one RPC about the adapter itself, beside the
5
+ * display's primary fetch. Three in the tree — HiC's `CoreGetInfo` header read,
6
+ * whose `resolutions` every contacts fetch waits on; the multi-sample sample
7
+ * list, which `fetchNeeded` declines until; and the tiered alignment file's
8
+ * `CoreGetInfo` header, which decides the LOD tier a synteny fetch asks for —
9
+ * and they sit on three different fetch foundations, which is why this sits in
10
+ * core beside the skeleton and not beside any of them.
11
+ *
12
+ * They had the shape below hand-copied, comment for comment, and each term is a
13
+ * rule rather than a preference:
14
+ *
15
+ * - **the adapter config is the whole trigger and the whole key.** Tracked in
16
+ * `prepare` so a config-editor edit re-reads; keyed on the same value so an
17
+ * un-minimize over the same file re-reads nothing. Getting one without the
18
+ * other gives either a stale header or a walk of a `.hic` norm-vector index on
19
+ * every expand.
20
+ * - **minimized is the gate**, and a display with a second condition of its own
21
+ * passes `gate` — the sample-list read waits for the LGV to be measured as
22
+ * well, so a full-file scan does not start ahead of the display's own first
23
+ * fetch, and the tier read waits on the track declaring a tier threshold at
24
+ * all.
25
+ * - **no `contract`.** This is the second fetch on a display whose foundation
26
+ * installed both checks already: `assertDisplayContract` would report the
27
+ * double-attach it exists to catch, and a second retry check would consume the
28
+ * same `reloadCounter` bumps as the first.
29
+ * - **where the status goes is the caller's** (`report`). The two LGV reads
30
+ * lend the display's own status window, so the read takes a slot beside the
31
+ * primary fetch rather than opening a second writer over the one status field
32
+ * (ADR-081) — and it matters more here than for most fetches: a prerequisite
33
+ * runs inside the pre-first-paint window, where the scrim is up because
34
+ * nothing is drawn rather than because `isLoading` is, so `statusMessage` is
35
+ * the only thing that can say what is happening. The tier read narrates
36
+ * nothing; a header is not a load.
37
+ *
38
+ * `createAdapterMetadataFetch` keys on the same adapter config and is
39
+ * deliberately NOT one of these: no trigger, no stop token and no status,
40
+ * because it is fetched lazily when a details widget opens and the widget opens
41
+ * on the result. A prerequisite read is the one a display's own fetch waits on.
42
+ *
43
+ * **A declaration, not the skeleton this name used to be.** There was an
44
+ * `installPrerequisiteFetch` until 2026-08-23, and `3c5aa7fce2` deleted it for
45
+ * a good reason: it carried a `runOne` of its own — a fifth copy of begin →
46
+ * clear → run → commit-if-current → `handleFetchError` → end — and once
47
+ * `installFetch` owned that sequence, a second one was pure drift surface. This
48
+ * one adds no sequence, no rotation and no error rule; it is terms handed to
49
+ * `installFetch`, the same shape `installGlobalFetchAutorun` and
50
+ * `installComparativeFetchAutorun` are. Add nothing here that a family could
51
+ * not state as an option to the skeleton.
52
+ *
53
+ * Everything the skeleton gives it is the point of going through the skeleton:
54
+ * the latest-wins rotation (so a superseding read aborts the one it replaced,
55
+ * and only a superseding read does — a user cancel of the primary fetch cannot
56
+ * strand the display the way sharing its token would), the clear at the start,
57
+ * the currency-guarded error rule, the unconditional `reloadCounter` read that
58
+ * makes the chrome's Retry re-run this, and the retired status slot that keeps a
59
+ * failed read from leaving a progress chip up for good.
60
+ */
61
+ export function installPrerequisiteFetch(self, { name, delay, report, gate = () => true, run, commit, setError, }) {
62
+ installFetch(self, {
63
+ name,
64
+ delay,
65
+ report,
66
+ gate: () => !self.isMinimized && gate(),
67
+ prepare: () => ({ adapterConfig: self.adapterConfig }),
68
+ fetchKey: ({ adapterConfig }) => adapterConfigKey(adapterConfig),
69
+ run: ({ adapterConfig }, ctx) => run(adapterConfig, ctx),
70
+ commit,
71
+ setError,
72
+ });
73
+ }
@@ -0,0 +1,13 @@
1
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ /**
3
+ * #api core/util
4
+ * Run a display's `afterAttach` body from a module loaded on demand, so the
5
+ * autorun installers stay out of the display's eager bundle.
6
+ *
7
+ * One policy for the gap the `await` opens: a node torn down before the module
8
+ * lands installs nothing, and a module that fails to load is reported where the
9
+ * user can see it rather than onto the display's own error slot — that slot is
10
+ * what `reload()` clears, and nothing would re-run the install behind it.
11
+ * Three displays hand-rolled this IIFE and each drew the lines differently.
12
+ */
13
+ export declare function runLazyAfterAttach<Self extends IStateTreeNode>(self: Self, load: () => Promise<(self: Self) => void>): void;
@@ -0,0 +1,31 @@
1
+ import { isAlive } from '@jbrowse/mobx-state-tree';
2
+ import { getNotificationSink } from './sessionServices.js';
3
+ /**
4
+ * #api core/util
5
+ * Run a display's `afterAttach` body from a module loaded on demand, so the
6
+ * autorun installers stay out of the display's eager bundle.
7
+ *
8
+ * One policy for the gap the `await` opens: a node torn down before the module
9
+ * lands installs nothing, and a module that fails to load is reported where the
10
+ * user can see it rather than onto the display's own error slot — that slot is
11
+ * what `reload()` clears, and nothing would re-run the install behind it.
12
+ * Three displays hand-rolled this IIFE and each drew the lines differently.
13
+ */
14
+ export function runLazyAfterAttach(self, load) {
15
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
16
+ ;
17
+ (async () => {
18
+ try {
19
+ const install = await load();
20
+ if (isAlive(self)) {
21
+ install(self);
22
+ }
23
+ }
24
+ catch (e) {
25
+ if (isAlive(self)) {
26
+ console.error(e);
27
+ getNotificationSink(self).notifyError(`${e}`, e);
28
+ }
29
+ }
30
+ })();
31
+ }
@@ -0,0 +1,50 @@
1
+ export interface LegendCandidate {
2
+ rowIndex: number;
3
+ label: string;
4
+ color: number;
5
+ }
6
+ export interface LegendEntry {
7
+ label: string;
8
+ color: number;
9
+ }
10
+ export declare const MAX_LEGEND_ENTRIES = 30;
11
+ export declare const MAX_LEGEND_CANDIDATES = 1024;
12
+ /**
13
+ * Accumulator for the distinct (row, name, color) combinations a packer walks
14
+ * past, in first-seen order and bounded — the small list `unionLegendCandidates`
15
+ * derives a key from, so the main thread never re-walks the features. Fed scalar
16
+ * by scalar, from the loop that already holds the three values.
17
+ *
18
+ * Deduped on the whole triple rather than on (row, color): the union takes the
19
+ * first NAME it sees for a color, so where that name is already spoken for, the
20
+ * second name on that color has to still be in the list to carry it.
21
+ *
22
+ * A nameless feature contributes nothing, which is the union's rule too — so a
23
+ * track whose features have no names ships an empty list rather than one entry
24
+ * per feature.
25
+ */
26
+ export declare function createLegendCandidateCollector(maxCandidates?: number): {
27
+ candidates: LegendCandidate[];
28
+ add(rowIndex: number, label: string, color: number): void;
29
+ };
30
+ export interface LegendCandidateSource {
31
+ candidates: readonly LegendCandidate[];
32
+ rowPaintsCandidateColor: (rowIndex: number) => boolean;
33
+ }
34
+ /**
35
+ * The categorical color key over any number of packed regions: one entry per
36
+ * distinct color, labeled by the first name seen in that color, in first-seen
37
+ * order.
38
+ *
39
+ * Keyed by color rather than by name, so each key row is 1:1 with a color — a
40
+ * row is normally a toggle and hiding a category hides features BY color, so two
41
+ * names sharing one color must collapse to a single row. A name reused across
42
+ * two colors keeps its first-seen color.
43
+ *
44
+ * `[]` when the data carries no categorical signal: nothing named, or more than
45
+ * `maxEntries` distinct colors.
46
+ *
47
+ * `regions` is consumed lazily, so a caller able to answer without reading any
48
+ * of them can pass a generator and have none of them touched.
49
+ */
50
+ export declare function unionLegendCandidates<T>(regions: Iterable<T>, resolve: (region: T) => LegendCandidateSource, maxEntries?: number): LegendEntry[];
@@ -0,0 +1,87 @@
1
+ // How many distinct colors a derived key may name and still BE a key. Past this
2
+ // the data is not a categorical vocabulary (a track keyed by unique per-feature
3
+ // names), and a list of every feature is the thing a key exists instead of.
4
+ //
5
+ // A different question from `legendIsReadable`'s lower bar, which asks whether a
6
+ // key a display already has is worth the rows it costs.
7
+ export const MAX_LEGEND_ENTRIES = 30;
8
+ // Bound on the candidate list one region ships. Far above MAX_LEGEND_ENTRIES, so
9
+ // a vocabulary that is categorical at all crosses whole, and a track with a name
10
+ // per feature ships a prefix that reads the same way it would have.
11
+ export const MAX_LEGEND_CANDIDATES = 1024;
12
+ /**
13
+ * Accumulator for the distinct (row, name, color) combinations a packer walks
14
+ * past, in first-seen order and bounded — the small list `unionLegendCandidates`
15
+ * derives a key from, so the main thread never re-walks the features. Fed scalar
16
+ * by scalar, from the loop that already holds the three values.
17
+ *
18
+ * Deduped on the whole triple rather than on (row, color): the union takes the
19
+ * first NAME it sees for a color, so where that name is already spoken for, the
20
+ * second name on that color has to still be in the list to carry it.
21
+ *
22
+ * A nameless feature contributes nothing, which is the union's rule too — so a
23
+ * track whose features have no names ships an empty list rather than one entry
24
+ * per feature.
25
+ */
26
+ export function createLegendCandidateCollector(maxCandidates = MAX_LEGEND_CANDIDATES) {
27
+ const candidates = [];
28
+ const colorsSeen = new Map();
29
+ return {
30
+ candidates,
31
+ add(rowIndex, label, color) {
32
+ if (label !== '' && candidates.length < maxCandidates) {
33
+ let byLabel = colorsSeen.get(rowIndex);
34
+ if (byLabel === undefined) {
35
+ byLabel = new Map();
36
+ colorsSeen.set(rowIndex, byLabel);
37
+ }
38
+ let colors = byLabel.get(label);
39
+ if (colors === undefined) {
40
+ colors = new Set();
41
+ byLabel.set(label, colors);
42
+ }
43
+ if (!colors.has(color)) {
44
+ colors.add(color);
45
+ candidates.push({ rowIndex, label, color });
46
+ }
47
+ }
48
+ },
49
+ };
50
+ }
51
+ /**
52
+ * The categorical color key over any number of packed regions: one entry per
53
+ * distinct color, labeled by the first name seen in that color, in first-seen
54
+ * order.
55
+ *
56
+ * Keyed by color rather than by name, so each key row is 1:1 with a color — a
57
+ * row is normally a toggle and hiding a category hides features BY color, so two
58
+ * names sharing one color must collapse to a single row. A name reused across
59
+ * two colors keeps its first-seen color.
60
+ *
61
+ * `[]` when the data carries no categorical signal: nothing named, or more than
62
+ * `maxEntries` distinct colors.
63
+ *
64
+ * `regions` is consumed lazily, so a caller able to answer without reading any
65
+ * of them can pass a generator and have none of them touched.
66
+ */
67
+ export function unionLegendCandidates(regions, resolve, maxEntries = MAX_LEGEND_ENTRIES) {
68
+ const entries = [];
69
+ const seenColors = new Set();
70
+ const seenLabels = new Set();
71
+ for (const region of regions) {
72
+ const { candidates, rowPaintsCandidateColor } = resolve(region);
73
+ for (const { rowIndex, label, color } of candidates) {
74
+ if (rowPaintsCandidateColor(rowIndex) &&
75
+ !seenLabels.has(label) &&
76
+ !seenColors.has(color)) {
77
+ seenLabels.add(label);
78
+ seenColors.add(color);
79
+ entries.push({ label, color });
80
+ if (entries.length > maxEntries) {
81
+ return [];
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return entries;
87
+ }