@jbrowse/core 5.0.0-beta.1 → 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 (115) hide show
  1. package/README.md +42 -3
  2. package/esm/BaseFeatureWidget/SequenceFeatureDetails/test_data/NCDN.d.ts +2 -2
  3. package/esm/PluginLoader.d.ts +1 -0
  4. package/esm/PluginLoader.js +52 -8
  5. package/esm/PluginManager.d.ts +70 -17
  6. package/esm/PluginManager.js +151 -20
  7. package/esm/ReExports/modules.js +3 -53
  8. package/esm/ReExports/publicUtil.d.ts +1 -1
  9. package/esm/ReExports/publicUtil.js +1 -1
  10. package/esm/ReExports/sharedModules.d.ts +96 -0
  11. package/esm/ReExports/sharedModules.js +59 -0
  12. package/esm/ReExports/uiStub.d.ts +1 -0
  13. package/esm/ReExports/uiStub.js +13 -0
  14. package/esm/ReExports/workerModules.d.ts +2 -0
  15. package/esm/ReExports/workerModules.js +10 -0
  16. package/esm/configuration/configurationSchema.d.ts +5 -38
  17. package/esm/configuration/configurationSchema.js +53 -40
  18. package/esm/configuration/promotableDefaults.js +12 -6
  19. package/esm/configuration/promotableResolve.d.ts +1 -1
  20. package/esm/configuration/promotableResolve.js +1 -1
  21. package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.d.ts +12 -0
  22. package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.js +39 -0
  23. package/esm/data_adapters/BaseAdapter/featureDensity.d.ts +29 -0
  24. package/esm/data_adapters/BaseAdapter/featureDensity.js +17 -0
  25. package/esm/data_adapters/BaseAdapter/index.d.ts +1 -0
  26. package/esm/data_adapters/BaseAdapter/index.js +1 -0
  27. package/esm/data_adapters/BaseAdapter/stats.d.ts +33 -1
  28. package/esm/data_adapters/BaseAdapter/stats.js +52 -13
  29. package/esm/data_adapters/BaseAdapter/types.d.ts +1 -0
  30. package/esm/pluggableElementTypes/DisplayType.d.ts +9 -10
  31. package/esm/pluggableElementTypes/DisplayType.js +43 -8
  32. package/esm/pluggableElementTypes/ViewType.d.ts +25 -2
  33. package/esm/pluggableElementTypes/ViewType.js +75 -2
  34. package/esm/pluggableElementTypes/extendElementType.js +8 -3
  35. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +41 -0
  36. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +74 -1
  37. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +21 -0
  38. package/esm/pluggableElementTypes/models/BaseTrackModel.js +37 -2
  39. package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +49 -0
  40. package/esm/pluggableElementTypes/models/BaseViewModel.js +39 -0
  41. package/esm/pluggableElementTypes/models/assertDisplayContract.d.ts +10 -7
  42. package/esm/pluggableElementTypes/models/assertDisplayContract.js +23 -30
  43. package/esm/rpc/RpcRegistry.d.ts +10 -0
  44. package/esm/rpc/byteBudget.d.ts +9 -0
  45. package/esm/rpc/coreRpcMethods.d.ts +1 -0
  46. package/esm/rpc/coreRpcMethods.js +1 -0
  47. package/esm/rpc/methods/CoreGetFeatureDensity.d.ts +11 -0
  48. package/esm/rpc/methods/CoreGetFeatureDensity.js +22 -0
  49. package/esm/svg/svgReady.d.ts +1 -0
  50. package/esm/ui/CascadingMenu.js +30 -8
  51. package/esm/ui/LoadingOverlay.d.ts +14 -0
  52. package/esm/ui/LoadingOverlay.js +1 -1
  53. package/esm/ui/MenuTypes.d.ts +11 -2
  54. package/esm/ui/MenuTypes.js +3 -0
  55. package/esm/ui/PaletteContext.d.ts +11 -4
  56. package/esm/ui/PaletteContext.js +34 -5
  57. package/esm/ui/PinAdornment.js +12 -6
  58. package/esm/ui/index.d.ts +1 -1
  59. package/esm/ui/index.js +1 -1
  60. package/esm/ui/launchViewMenu.js +7 -1
  61. package/esm/ui/legendMenuItem.d.ts +5 -5
  62. package/esm/ui/legendMenuItem.js +5 -5
  63. package/esm/ui/makeSizeMenu.js +1 -1
  64. package/esm/ui/menuItems.d.ts +1 -1
  65. package/esm/ui/menuItems.js +1 -1
  66. package/esm/ui/promotableMenuItems.js +8 -6
  67. package/esm/ui/promotablePinCoverage.js +2 -1
  68. package/esm/ui/toggleMenuItems.d.ts +1 -3
  69. package/esm/util/addAndShowTrack.d.ts +1 -1
  70. package/esm/util/addAndShowTrack.js +5 -1
  71. package/esm/util/addTrackFromWidget.js +2 -2
  72. package/esm/util/cacheBudgets.d.ts +22 -19
  73. package/esm/util/cacheBudgets.js +22 -19
  74. package/esm/util/createStopTokenRotation.d.ts +1 -0
  75. package/esm/util/createStopTokenRotation.js +7 -27
  76. package/esm/util/fetchContext.d.ts +2 -2
  77. package/esm/util/fetchContext.js +2 -2
  78. package/esm/util/fetchPhases.d.ts +9 -9
  79. package/esm/util/hooks.d.ts +8 -0
  80. package/esm/util/hooks.js +27 -0
  81. package/esm/util/index.d.ts +2 -3
  82. package/esm/util/index.js +2 -3
  83. package/esm/util/installFetch.d.ts +49 -23
  84. package/esm/util/installFetch.js +49 -21
  85. package/esm/util/installInitAutorun.d.ts +8 -8
  86. package/esm/util/installInitAutorun.js +15 -15
  87. package/esm/util/lazyAfterAttach.d.ts +13 -0
  88. package/esm/util/lazyAfterAttach.js +31 -0
  89. package/esm/util/legendCandidates.d.ts +50 -0
  90. package/esm/util/legendCandidates.js +87 -0
  91. package/esm/util/mstUtils.d.ts +12 -1
  92. package/esm/util/mstUtils.js +20 -2
  93. package/esm/util/pluginStore.d.ts +1 -0
  94. package/esm/util/pluginStore.js +8 -1
  95. package/esm/util/range.d.ts +13 -9
  96. package/esm/util/range.js +17 -8
  97. package/esm/util/rxjs.d.ts +16 -0
  98. package/esm/util/rxjs.js +49 -0
  99. package/esm/util/tabix.d.ts +10 -4
  100. package/esm/util/tabix.js +11 -5
  101. package/esm/util/tracks.d.ts +48 -0
  102. package/esm/util/tracks.js +192 -66
  103. package/esm/util/types/index.d.ts +48 -5
  104. package/esm/util/types/index.js +3 -0
  105. package/esm/util/unknownSnapshotKeys.d.ts +36 -0
  106. package/esm/util/unknownSnapshotKeys.js +69 -0
  107. package/esm/util/viewStatus.d.ts +23 -0
  108. package/esm/util/viewStatus.js +17 -0
  109. package/esm/util/whenViewSettled.d.ts +17 -4
  110. package/esm/util/whenViewSettled.js +19 -6
  111. package/esm/util/withLaunchInput.d.ts +123 -0
  112. package/esm/util/withLaunchInput.js +228 -0
  113. package/package.json +12 -9
  114. package/esm/util/applyInitSettings.d.ts +0 -79
  115. package/esm/util/applyInitSettings.js +0 -91
@@ -1,5 +1,5 @@
1
1
  import { hasParent, isAlive, types } from '@jbrowse/mobx-state-tree';
2
- import { getConf } from '../../configuration/index.js';
2
+ import { getConf, isConfigurationSlot, preProcessSlotValues, } from '../../configuration/index.js';
3
3
  import { getContainingTrack, getEnv, statusFraction, statusMessageText, } from '../../util/index.js';
4
4
  import { ElementId } from '../../util/types/mst.js';
5
5
  /**
@@ -210,6 +210,79 @@ function stateModelFactory() {
210
210
  * base display reload does nothing, see specialized displays for details
211
211
  */
212
212
  reload() { },
213
+ /**
214
+ * #action
215
+ * Apply a bag of display settings to the LIVE display, and report what
216
+ * landed. Each key runs through the display config schema's
217
+ * `preProcessSnapshot` (shorthand expansions, legacy-key migrations —
218
+ * the same lowering a session spec's inline track keys get in
219
+ * `showTrackGeneric`), then writes the matching config slot. Keys that
220
+ * are not slots come back in `unapplied` rather than vanishing: the
221
+ * settings vocabulary's historical failure mode is the silently dropped
222
+ * key.
223
+ *
224
+ * `allowSetters` additionally routes a non-slot key to a conventionally
225
+ * named single-argument `set<Key>` action. Opt-in, never the default:
226
+ * the declarative surfaces (session specs, share links, embeds) feed
227
+ * this whole bags of untyped JSON, and a blanket fallback would let
228
+ * them reach internal setters (`setError`, `setScrollTop`, ...) and
229
+ * call multi-argument setters with one argument. A caller that wants a
230
+ * specific action can also simply call it.
231
+ *
232
+ * A key whose write THREW is reported separately, in `failed` — it is the
233
+ * only one of the three that means the caller got something wrong, and
234
+ * the only one worth a notification. `unapplied` is not: at the
235
+ * `showTrackGeneric` call site it also collects keys that surface
236
+ * consumed itself (`type`) and MST display props the display snapshot
237
+ * already applied (`resolution`), so treating it as "dropped" would
238
+ * report a correct call as broken.
239
+ *
240
+ * Per-key errors do not abort the rest of the bag — a caller
241
+ * mid-`showTrack` has already pushed the track, and one rejected value
242
+ * must not strand a half-configured track.
243
+ */
244
+ applyDisplaySettings(settings, options) {
245
+ // configuration is the reference every concrete display adds at
246
+ // instantiation (see DisplayModel below); the base model composes
247
+ // before it exists, hence the cast rather than a prop
248
+ const { configuration } = self;
249
+ const applied = [];
250
+ const unapplied = [];
251
+ const failed = [];
252
+ const slots = preProcessSlotValues(configuration, settings);
253
+ for (const [key, value] of Object.entries(slots)) {
254
+ if (!key || key === 'type') {
255
+ unapplied.push(key ? 'type (switch the display type instead)' : '(empty key)');
256
+ continue;
257
+ }
258
+ try {
259
+ if (isConfigurationSlot(configuration, key)) {
260
+ // the key arrives from runtime JSON; setConf's slot name is a
261
+ // compile-time type
262
+ // eslint-disable-next-line no-restricted-syntax
263
+ configuration.setSlot(key, value);
264
+ applied.push(key);
265
+ continue;
266
+ }
267
+ const setter = self[`set${key[0].toUpperCase()}${key.slice(1)}`];
268
+ if (typeof setter !== 'function') {
269
+ unapplied.push(key);
270
+ }
271
+ else if (options?.allowSetters) {
272
+ ;
273
+ setter(value);
274
+ applied.push(`${key} (via setter)`);
275
+ }
276
+ else {
277
+ unapplied.push(`${key} (not a config slot; a set${key[0].toUpperCase()}${key.slice(1)} action exists — call it, or pass { allowSetters: true })`);
278
+ }
279
+ }
280
+ catch (e) {
281
+ failed.push({ key, error: `${e}` });
282
+ }
283
+ }
284
+ return { applied, unapplied, failed };
285
+ },
213
286
  }));
214
287
  }
215
288
  export const BaseDisplay = stateModelFactory();
@@ -144,6 +144,27 @@ export declare function createBaseTrackModel(pm: PluginManager, trackType: strin
144
144
  * #action
145
145
  */
146
146
  setResizing(flag: boolean): void;
147
+ /**
148
+ * #action
149
+ * `applyDisplaySettings` on the display being drawn (`activeDisplay`,
150
+ * which a shown track always has — this is not meaningful on a bare
151
+ * config node) — the track-level entry for "restyle this track in
152
+ * place" (a session spec's inline keys, an agent's settings bag). Only
153
+ * that one display: settings vocabularies are per display type, so
154
+ * broadcasting one bag across a track's other displays would mis-route
155
+ * keys; address a non-active display directly if that is what you mean.
156
+ * See BaseDisplayModel for the routing and the `allowSetters` opt-in.
157
+ */
158
+ applyDisplaySettings(settings: Record<string, unknown>, options?: {
159
+ allowSetters?: boolean;
160
+ }): {
161
+ applied: string[];
162
+ unapplied: string[];
163
+ failed: {
164
+ key: string;
165
+ error: string;
166
+ }[];
167
+ };
147
168
  /**
148
169
  * #action
149
170
  */
@@ -1,5 +1,5 @@
1
1
  import { lazy } from 'react';
2
- import { addDisposer, getSnapshot, types } from '@jbrowse/mobx-state-tree';
2
+ import { addDisposer, getSnapshot, isAlive, types, } from '@jbrowse/mobx-state-tree';
3
3
  import Save from '@mui/icons-material/Save';
4
4
  import { compareStructural, reaction } from 'mobx';
5
5
  import { ConfigurationReference, getConf } from '../../configuration/index.js';
@@ -251,6 +251,20 @@ export function createBaseTrackModel(pm, trackType, baseTrackConfig) {
251
251
  setResizing(flag) {
252
252
  self.resizing = flag;
253
253
  },
254
+ /**
255
+ * #action
256
+ * `applyDisplaySettings` on the display being drawn (`activeDisplay`,
257
+ * which a shown track always has — this is not meaningful on a bare
258
+ * config node) — the track-level entry for "restyle this track in
259
+ * place" (a session spec's inline keys, an agent's settings bag). Only
260
+ * that one display: settings vocabularies are per display type, so
261
+ * broadcasting one bag across a track's other displays would mis-route
262
+ * keys; address a non-active display directly if that is what you mean.
263
+ * See BaseDisplayModel for the routing and the `allowSetters` opt-in.
264
+ */
265
+ applyDisplaySettings(settings, options) {
266
+ return self.activeDisplay.applyDisplaySettings(settings, options);
267
+ },
254
268
  /**
255
269
  * #action
256
270
  */
@@ -261,6 +275,12 @@ export function createBaseTrackModel(pm, trackType, baseTrackConfig) {
261
275
  }
262
276
  const displays = self.configuration.displays;
263
277
  const displayConf = getDisplayConf(displays, newDisplayId);
278
+ // same interception showTrackGeneric makes: a registered-but-unloaded
279
+ // display would otherwise fail as an opaque union mismatch below
280
+ const displayRecord = pm.resolveDisplayTypeRecord(displayConf.type);
281
+ if (displayRecord && !displayRecord.isStateModelLoaded) {
282
+ throw new Error(`state model for display type ${displayConf.type} is not loaded yet — await getDisplayType('${displayConf.type}').loadStateModel() first`);
283
+ }
264
284
  self.displays[idx] = {
265
285
  ...initialSnapshot,
266
286
  type: displayConf.type,
@@ -434,9 +454,24 @@ export function createBaseTrackModel(pm, trackType, baseTrackConfig) {
434
454
  // being replaced, so leaving the menu up would keep a
435
455
  // list of items built against a destroyed MST node
436
456
  keepMenuOpen: false,
457
+ // the chosen display's state model may still be a dynamic
458
+ // import away, so load it before swapping — replaceDisplay
459
+ // itself stays a plain sync action
437
460
  onClick: () => {
438
461
  if (d.displayId !== shownId) {
439
- self.replaceDisplay(shownId, d.displayId, self.activeDisplay.getPortableSettings?.(d.displayId) ?? {});
462
+ void (async () => {
463
+ try {
464
+ await pm
465
+ .resolveDisplayTypeRecord(d.type)
466
+ ?.loadStateModel();
467
+ if (isAlive(self)) {
468
+ self.replaceDisplay(shownId, d.displayId, self.activeDisplay.getPortableSettings?.(d.displayId) ?? {});
469
+ }
470
+ }
471
+ catch (e) {
472
+ getSession(self).notifyError(`${e}`, e);
473
+ }
474
+ })();
440
475
  }
441
476
  },
442
477
  };
@@ -1,4 +1,5 @@
1
1
  import type { MenuItem } from '../../ui/index.ts';
2
+ import type { AbstractTrackModel, AbstractViewModel } from '../../util/types/index.ts';
2
3
  import type { Instance } from '@jbrowse/mobx-state-tree';
3
4
  /**
4
5
  * #stateModel BaseViewModel
@@ -66,6 +67,30 @@ declare const BaseViewModel: import("@jbrowse/mobx-state-tree").IModelType<{
66
67
  * #method
67
68
  */
68
69
  menuItems(): MenuItem[];
70
+ /**
71
+ * #getter
72
+ * Every track open on this view itself: its own `tracks` array plus any
73
+ * track containers it owns instead (the synteny view keeps one list per
74
+ * band on `trackContainers` and its own `tracks` empty). Tracks on nested
75
+ * views are `allTracks`'s, not this getter's.
76
+ */
77
+ readonly ownTracks: AbstractTrackModel[];
78
+ /**
79
+ * #getter
80
+ * This view and every view nested inside it, to any depth — a synteny
81
+ * stack's genome rows, a breakpoint split view's panels. Each view answers
82
+ * for its own children, so a consumer never walks the nesting itself:
83
+ * before this getter, four consumers each carried a copy of the walk and
84
+ * two of them had drifted (one read `levels`, one read `trackContainers`,
85
+ * and each was blind to the other's spelling).
86
+ */
87
+ readonly allViews: AbstractViewModel[];
88
+ } & {
89
+ /**
90
+ * #getter
91
+ * Every track open on this view or any view nested inside it.
92
+ */
93
+ readonly allTracks: AbstractTrackModel[];
69
94
  } & {
70
95
  /**
71
96
  * #action
@@ -172,6 +197,30 @@ export declare const BaseViewModelWithDisplayedRegions: import("@jbrowse/mobx-st
172
197
  * #method
173
198
  */
174
199
  menuItems(): MenuItem[];
200
+ /**
201
+ * #getter
202
+ * Every track open on this view itself: its own `tracks` array plus any
203
+ * track containers it owns instead (the synteny view keeps one list per
204
+ * band on `trackContainers` and its own `tracks` empty). Tracks on nested
205
+ * views are `allTracks`'s, not this getter's.
206
+ */
207
+ readonly ownTracks: AbstractTrackModel[];
208
+ /**
209
+ * #getter
210
+ * This view and every view nested inside it, to any depth — a synteny
211
+ * stack's genome rows, a breakpoint split view's panels. Each view answers
212
+ * for its own children, so a consumer never walks the nesting itself:
213
+ * before this getter, four consumers each carried a copy of the walk and
214
+ * two of them had drifted (one read `levels`, one read `trackContainers`,
215
+ * and each was blind to the other's spelling).
216
+ */
217
+ readonly allViews: AbstractViewModel[];
218
+ } & {
219
+ /**
220
+ * #getter
221
+ * Every track open on this view or any view nested inside it.
222
+ */
223
+ readonly allTracks: AbstractTrackModel[];
175
224
  } & {
176
225
  /**
177
226
  * #action
@@ -94,6 +94,45 @@ const BaseViewModel = types
94
94
  menuItems() {
95
95
  return [];
96
96
  },
97
+ /**
98
+ * #getter
99
+ * Every track open on this view itself: its own `tracks` array plus any
100
+ * track containers it owns instead (the synteny view keeps one list per
101
+ * band on `trackContainers` and its own `tracks` empty). Tracks on nested
102
+ * views are `allTracks`'s, not this getter's.
103
+ */
104
+ get ownTracks() {
105
+ const s = self;
106
+ return [
107
+ ...(s.tracks ?? []),
108
+ ...(s.trackContainers ?? []).flatMap(c => c.tracks ?? []),
109
+ ];
110
+ },
111
+ /**
112
+ * #getter
113
+ * This view and every view nested inside it, to any depth — a synteny
114
+ * stack's genome rows, a breakpoint split view's panels. Each view answers
115
+ * for its own children, so a consumer never walks the nesting itself:
116
+ * before this getter, four consumers each carried a copy of the walk and
117
+ * two of them had drifted (one read `levels`, one read `trackContainers`,
118
+ * and each was blind to the other's spelling).
119
+ */
120
+ get allViews() {
121
+ const s = self;
122
+ return [
123
+ self,
124
+ ...(s.views ?? []).flatMap(v => v.allViews ?? []),
125
+ ];
126
+ },
127
+ }))
128
+ .views(self => ({
129
+ /**
130
+ * #getter
131
+ * Every track open on this view or any view nested inside it.
132
+ */
133
+ get allTracks() {
134
+ return self.allViews.flatMap(v => v.ownTracks);
135
+ },
97
136
  }))
98
137
  .actions(self => ({
99
138
  /**
@@ -1,8 +1,10 @@
1
1
  import type { IAnyStateTreeNode } from '@jbrowse/mobx-state-tree';
2
2
  /**
3
- * Dev-only check that a display's fetch foundation attached it exactly once —
4
- * the one ordering contract here that is a *state* rather than a declaration,
5
- * so no selector can reach it. No-op in production.
3
+ * Checks that a display's fetch foundation attached it exactly once — the one
4
+ * ordering contract here that is a *state* rather than a declaration, so no
5
+ * selector can reach it. Costs a `WeakSet` lookup, so it runs in a production
6
+ * build too and reports through `contractReports`, which is silent until
7
+ * something arms it.
6
8
  *
7
9
  * Called once per display from **whichever fetch foundation installed its
8
10
  * autoruns** — `MultiRegionDisplayMixin`'s `afterAttach` for the per-region
@@ -69,14 +71,15 @@ interface RetryContractHost {
69
71
  */
70
72
  viewportEmpty?: boolean;
71
73
  }
72
- /** Called by `FetchMixin.runFetch`. Dev-only. */
74
+ /** Called by `FetchMixin.runFetch`. */
73
75
  export declare function noteFetchStarted(self: object): void;
74
76
  /** Reads and clears the flag above, so each check starts from a known state. */
75
77
  export declare function takeFetchStarted(self: object): boolean;
76
78
  /**
77
- * Dev-only check that a display's `reload()` can actually reach a fetch — the
78
- * retry contract, which `DisplayErrorBar` depends on and which no type can
79
- * state. No-op in production.
79
+ * Checks that a display's `reload()` can actually reach a fetch — the retry
80
+ * contract, which `DisplayErrorBar` depends on and which no type can state.
81
+ * Costs an `untracked` read per autorun run against a fetch, so it runs in a
82
+ * production build too and reports through `contractReports`.
80
83
  *
81
84
  * **The button is the contract.** `DisplayErrorBar`'s only action is
82
85
  * `model.reload()`, so every state that can raise the error bar has to be one
@@ -1,19 +1,19 @@
1
1
  import { getMembers } from '@jbrowse/mobx-state-tree';
2
+ import { reportContractViolation } from '@jbrowse/render-core/contractReports';
2
3
  import { untracked } from 'mobx';
3
4
  // Nodes whose fetch foundation has already installed its autoruns. Keyed on the
4
5
  // node rather than the model type: every display of a type would otherwise
5
6
  // report from the second instance onward.
6
7
  const attached = new WeakSet();
7
8
  function report(message) {
8
- // `console.error`, never `throw`: an error escaping `afterAttach` is read by
9
- // the session loader as an invalid track and the display is silently dropped
10
- // — which would hide the very contract violation this is reporting.
11
- console.error(`[jbrowse display contract] ${message}`);
9
+ reportContractViolation('display', message);
12
10
  }
13
11
  /**
14
- * Dev-only check that a display's fetch foundation attached it exactly once —
15
- * the one ordering contract here that is a *state* rather than a declaration,
16
- * so no selector can reach it. No-op in production.
12
+ * Checks that a display's fetch foundation attached it exactly once — the one
13
+ * ordering contract here that is a *state* rather than a declaration, so no
14
+ * selector can reach it. Costs a `WeakSet` lookup, so it runs in a production
15
+ * build too and reports through `contractReports`, which is silent until
16
+ * something arms it.
17
17
  *
18
18
  * Called once per display from **whichever fetch foundation installed its
19
19
  * autoruns** — `MultiRegionDisplayMixin`'s `afterAttach` for the per-region
@@ -46,19 +46,17 @@ function report(message) {
46
46
  * points nowhere near the cause.
47
47
  */
48
48
  export function assertDisplayContract(self, installedBy = "the per-region fetch foundation's afterAttach") {
49
- if (process.env.NODE_ENV !== 'production') {
50
- if (attached.has(self)) {
51
- report(`${getMembers(self).name}: ${installedBy} ran twice on one display, ` +
52
- `so its fetch autoruns are installed twice (double fetches, double ` +
53
- `clears). Our MST fork auto-chains lifecycle hooks, so the usual ` +
54
- `cause is a superAfterAttach() call in this display's afterAttach — ` +
55
- `delete it. Otherwise this display composes two fetch foundations, ` +
56
- `or calls an installer it already gets from a mixin. ` +
57
- `See agent-docs/ARCHITECTURE.md §"What not to do".`);
58
- }
59
- else {
60
- attached.add(self);
61
- }
49
+ if (attached.has(self)) {
50
+ report(`${getMembers(self).name}: ${installedBy} ran twice on one display, ` +
51
+ `so its fetch autoruns are installed twice (double fetches, double ` +
52
+ `clears). Our MST fork auto-chains lifecycle hooks, so the usual ` +
53
+ `cause is a superAfterAttach() call in this display's afterAttach — ` +
54
+ `delete it. Otherwise this display composes two fetch foundations, ` +
55
+ `or calls an installer it already gets from a mixin. ` +
56
+ `See agent-docs/ARCHITECTURE.md §"What not to do".`);
57
+ }
58
+ else {
59
+ attached.add(self);
62
60
  }
63
61
  }
64
62
  // Displays whose fetch has started since the last time the caller looked, and
@@ -85,11 +83,8 @@ export function assertDisplayContract(self, installedBy = "the per-region fetch
85
83
  // on the display with the liveliest one.
86
84
  const fetchStarted = new WeakSet();
87
85
  const ledgers = new WeakMap();
88
- /** Called by `FetchMixin.runFetch`. Dev-only. */
86
+ /** Called by `FetchMixin.runFetch`. */
89
87
  export function noteFetchStarted(self) {
90
- if (process.env.NODE_ENV === 'production') {
91
- return;
92
- }
93
88
  fetchStarted.add(self);
94
89
  ledgers.get(self)?.('fetched');
95
90
  }
@@ -98,9 +93,10 @@ export function takeFetchStarted(self) {
98
93
  return fetchStarted.delete(self);
99
94
  }
100
95
  /**
101
- * Dev-only check that a display's `reload()` can actually reach a fetch — the
102
- * retry contract, which `DisplayErrorBar` depends on and which no type can
103
- * state. No-op in production.
96
+ * Checks that a display's `reload()` can actually reach a fetch — the retry
97
+ * contract, which `DisplayErrorBar` depends on and which no type can state.
98
+ * Costs an `untracked` read per autorun run against a fetch, so it runs in a
99
+ * production build too and reports through `contractReports`.
104
100
  *
105
101
  * **The button is the contract.** `DisplayErrorBar`'s only action is
106
102
  * `model.reload()`, so every state that can raise the error bar has to be one
@@ -195,9 +191,6 @@ export function takeFetchStarted(self) {
195
191
  * is worse than the bug being checked for.
196
192
  */
197
193
  export function makeRetryContractCheck(self) {
198
- if (process.env.NODE_ENV === 'production') {
199
- return () => { };
200
- }
201
194
  // eslint-disable-next-line no-restricted-syntax -- instrumentation
202
195
  let lastCounter = untracked(() => self.reloadCounter);
203
196
  function noteRetryContractOutcome(outcome) {
@@ -1,3 +1,4 @@
1
+ import type { FeatureDensity } from '../data_adapters/BaseAdapter/featureDensity.ts';
1
2
  import type { StatusCallback } from '../util/progress.ts';
2
3
  import type { UnwrapRpcResult } from '../util/rpc.ts';
3
4
  import type { Feature, SimpleFeatureSerialized } from '../util/simpleFeature.ts';
@@ -52,6 +53,15 @@ export interface RpcRegistry {
52
53
  };
53
54
  return: number | undefined;
54
55
  };
56
+ CoreGetFeatureDensity: {
57
+ args: {
58
+ adapterConfig: Record<string, unknown>;
59
+ regions: RegionLike[];
60
+ bpPerPx: number;
61
+ headers?: Record<string, string>;
62
+ };
63
+ return: FeatureDensity[] | undefined;
64
+ };
55
65
  CoreGetInfo: {
56
66
  args: {
57
67
  adapterConfig: Record<string, unknown>;
@@ -6,6 +6,15 @@ import type { AugmentedRegion } from '../util/types/data.ts';
6
6
  * budget, or the save dialog's bound on the whole download.
7
7
  */
8
8
  export type ByteEstimateScope = 'largestRegion' | 'wholeRequest';
9
+ /**
10
+ * The one spelling of a gated feature RPC's budget argument: the number the
11
+ * worker measures against, undefined when the gate declined to act and nothing
12
+ * is measured. A call-site argument, never part of `rpcProps`, because it
13
+ * swings at 20kb and on force-load.
14
+ */
15
+ export interface GatedFetchArgs {
16
+ byteLimit?: number;
17
+ }
9
18
  export interface ByteMeasurableAdapter {
10
19
  getRegionByteSize: (regions: AugmentedRegion[], opts?: {
11
20
  stopToken?: StopToken;
@@ -5,5 +5,6 @@ export { default as CoreGetFeatures } from './methods/CoreGetFeatures.ts';
5
5
  export { default as CoreGetSequence } from './methods/CoreGetSequence.ts';
6
6
  export { default as CoreFreeResources } from './methods/CoreFreeResources.ts';
7
7
  export { default as CoreGetRegionByteEstimate } from './methods/CoreGetRegionByteEstimate.ts';
8
+ export { default as CoreGetFeatureDensity } from './methods/CoreGetFeatureDensity.ts';
8
9
  export { default as CoreGetRegions } from './methods/CoreGetRegions.ts';
9
10
  export { default as CoreGetExportData } from './methods/CoreGetExportData.ts';
@@ -5,5 +5,6 @@ export { default as CoreGetFeatures } from './methods/CoreGetFeatures.js';
5
5
  export { default as CoreGetSequence } from './methods/CoreGetSequence.js';
6
6
  export { default as CoreFreeResources } from './methods/CoreFreeResources.js';
7
7
  export { default as CoreGetRegionByteEstimate } from './methods/CoreGetRegionByteEstimate.js';
8
+ export { default as CoreGetFeatureDensity } from './methods/CoreGetFeatureDensity.js';
8
9
  export { default as CoreGetRegions } from './methods/CoreGetRegions.js';
9
10
  export { default as CoreGetExportData } from './methods/CoreGetExportData.js';
@@ -0,0 +1,11 @@
1
+ import RpcMethodTypeWithRenameRegions from '../../pluggableElementTypes/RpcMethodTypeWithRenameRegions.ts';
2
+ import type { RpcExecuteArgs } from '../RpcRegistry.ts';
3
+ /**
4
+ * The density tier's read: features per bin at the view's bp/px for regions the
5
+ * region-too-large gate refused. Answers `undefined` for an adapter with no
6
+ * density source, and the display keeps the banner.
7
+ */
8
+ export default class CoreGetFeatureDensity extends RpcMethodTypeWithRenameRegions<'CoreGetFeatureDensity'> {
9
+ name: 'CoreGetFeatureDensity';
10
+ execute(args: RpcExecuteArgs<'CoreGetFeatureDensity'>): Promise<import("../../data_adapters/BaseAdapter/featureDensity.ts").FeatureDensity[] | undefined>;
11
+ }
@@ -0,0 +1,22 @@
1
+ import { isFeatureAdapter } from '../../data_adapters/BaseAdapter/index.js';
2
+ import { getAdapter } from '../../data_adapters/dataAdapterCache.js';
3
+ import RpcMethodTypeWithRenameRegions from '../../pluggableElementTypes/RpcMethodTypeWithRenameRegions.js';
4
+ /**
5
+ * The density tier's read: features per bin at the view's bp/px for regions the
6
+ * region-too-large gate refused. Answers `undefined` for an adapter with no
7
+ * density source, and the display keeps the banner.
8
+ */
9
+ export default class CoreGetFeatureDensity extends RpcMethodTypeWithRenameRegions {
10
+ name = 'CoreGetFeatureDensity';
11
+ async execute(args) {
12
+ const { adapterConfig, sessionId, regions, bpPerPx, stopToken } = args;
13
+ const { dataAdapter } = await getAdapter(this.pluginManager, sessionId, adapterConfig);
14
+ return isFeatureAdapter(dataAdapter)
15
+ ? dataAdapter.getFeatureDensity(regions, {
16
+ bpPerPx,
17
+ stopToken,
18
+ statusCallback: args.statusCallback,
19
+ })
20
+ : undefined;
21
+ }
22
+ }
@@ -94,6 +94,7 @@ export declare function awaitSvgReady(model: Pick<SvgExportable, 'svgReady' | 'e
94
94
  export declare function awaitViewInitialized(view: {
95
95
  initialized: boolean;
96
96
  error: unknown;
97
+ pendingLaunch?: unknown;
97
98
  }): Promise<void>;
98
99
  /**
99
100
  * How the export answers "a track failed": by failing. A track whose data won't
@@ -6,7 +6,7 @@ import { makeStyles } from '../util/tss-react/index.js';
6
6
  import { useEventCallback } from '../util/useEventCallback.js';
7
7
  import HoverMenu from './HoverMenu.js';
8
8
  import { MenuItemTrailing } from './MenuItemTrailing.js';
9
- import { staysOpenOnClick } from './MenuTypes.js';
9
+ import { resolveSubMenu, staysOpenOnClick } from './MenuTypes.js';
10
10
  import { hasMenuItemAdornment } from './menuItemAdornment.js';
11
11
  import { isAimedAtPanel } from './submenuAim.js';
12
12
  // Compact section headers so tall menus (e.g. a display's flat "Show..."
@@ -24,6 +24,12 @@ const useStyles = makeStyles()(theme => ({
24
24
  borderTop: `1px solid ${theme.palette.divider}`,
25
25
  marginTop: 4,
26
26
  },
27
+ // a leading subHeader has no rows above it to divide from, so the rule and
28
+ // its space would float against the menu's top edge
29
+ leadingSubHeader: {
30
+ borderTopWidth: 0,
31
+ marginTop: 0,
32
+ },
27
33
  }));
28
34
  const CascadingMenuContext = createContext(undefined);
29
35
  function useCascadingMenu() {
@@ -312,17 +318,36 @@ function CascadingSubmenu({ item, itemKey, inset, columns, hover, onNavigateBack
312
318
  // up
313
319
  paperRef: isOpen ? hover.panelRef : undefined, onClose: () => {
314
320
  hover.setSubmenu(undefined);
315
- }, children: _jsx(CascadingMenuList, { menuItems: item.subMenu, onNavigateBack: () => {
321
+ }, children: _jsx(SubMenuList, { item: item, onNavigateBack: () => {
316
322
  hover.setSubmenu(undefined);
317
323
  anchorEl?.focus();
318
324
  } }) })] }));
319
325
  }
326
+ // Resolves the rows inside the panel, which MUI mounts only while it is open:
327
+ // a function-form `subMenu` is not called by listing the parent menu. An
328
+ // observer so the observables that builder reads re-render the open panel, the
329
+ // way an array built in the root observer's render already did.
330
+ const SubMenuList = observer(function SubMenuList({ item, onNavigateBack, }) {
331
+ return (_jsx(CascadingMenuList, { menuItems: resolveSubMenu(item), onNavigateBack: onNavigateBack }));
332
+ });
320
333
  // One clickable menu row: label (with optional leading icon) plus its trailing
321
334
  // value/help/adornment decorations. The menu-wide `columns` flags let every row
322
335
  // reserve matching decoration slots so the columns line up down the menu.
323
336
  function CascadingMenuItem({ item, inset, columns, onNavigateBack, onHover, }) {
324
337
  const { closeAfterItemClick, onMenuItemClick, onCloseRoot } = useCascadingMenu();
325
- return (_jsx(DisabledTooltip, { item: item, children: _jsxs(MenuItem, { "data-testid": makeTestId('menuitem', item.label), disabled: item.disabled, onClick: () => {
338
+ return (_jsx(DisabledTooltip, { item: item, children: _jsxs(MenuItem, { "data-testid": makeTestId('menuitem', item.label), disabled: item.disabled,
339
+ // A checkbox or radio row states its own value. The glyph
340
+ // `MenuItemEndDecoration` swaps is an `aria-hidden` icon, so without
341
+ // these a screen reader reads "Curved lines" identically whether the
342
+ // setting is on or off, and a test has no way to ask but the MUI icon's
343
+ // internal testid.
344
+ role: item.type === 'checkbox'
345
+ ? 'menuitemcheckbox'
346
+ : item.type === 'radio'
347
+ ? 'menuitemradio'
348
+ : undefined, "aria-checked": item.type === 'checkbox' || item.type === 'radio'
349
+ ? item.checked
350
+ : undefined, onClick: () => {
326
351
  // onCloseRoot runs before the callback, so item.onClick must NOT read
327
352
  // model state that closing clears (e.g. a right-click menu's ephemeral
328
353
  // hit/context fields): capture that state when the menu items are
@@ -339,7 +364,7 @@ function CascadingMenuItem({ item, inset, columns, onNavigateBack, onHover, }) {
339
364
  }, children: [_jsx(MenuItemLeadingIcon, { Icon: item.icon }), _jsx(ListItemText, { primary: item.label, secondary: item.subLabel, inset: inset }), _jsx(MenuItemTrailing, { item: item, columns: columns })] }) }));
340
365
  }
341
366
  function CascadingMenuList({ menuItems, onNavigateBack, }) {
342
- const { classes } = useStyles();
367
+ const { classes, cx } = useStyles();
343
368
  const hover = useSubmenuHover();
344
369
  // every row a pointer can rest on asks for the open panel to go away; only a
345
370
  // row that owns a submenu, or the panel itself, keeps one up. Whether the ask
@@ -360,10 +385,7 @@ function CascadingMenuList({ menuItems, onNavigateBack, }) {
360
385
  return _jsx(Divider, { component: "li" }, `divider-${idx}`);
361
386
  }
362
387
  if (item.type === 'subHeader') {
363
- return (_jsx(ListSubheader, { className: classes.subHeader, onMouseEnter: closeOnHover,
364
- // a leading subHeader has no rows above it to divide from, so drop
365
- // the divider rule that would otherwise float at the menu's top edge
366
- sx: idx === 0 ? { borderTop: 'none', marginTop: 0 } : undefined, children: item.label }, `subHeader-${item.label}`));
388
+ return (_jsx(ListSubheader, { className: cx(classes.subHeader, idx === 0 && classes.leadingSubHeader), onMouseEnter: closeOnHover, children: item.label }, `subHeader-${item.label}`));
367
389
  }
368
390
  if (item.type === 'custom') {
369
391
  return (_jsx(CustomMenuRow, { item: item, onHover: closeOnHover }, `custom-${item.label}`));
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Returns false, then flips true once `active` has stayed true continuously for
3
+ * `delayMs`. Each activation gets a fresh delay, and the flag drops the moment
4
+ * `active` does.
5
+ *
6
+ * Keyed on a count of RISING edges rather than on `active` itself, so the
7
+ * falling edge does no work at all. A display refetching through a wheel zoom
8
+ * flips `active` several times a second — it finishes a fetch, waits out the
9
+ * debounce, starts the next — and an effect depending on `active` tore its timer
10
+ * down and built a new one on each of those, twice over, for a scrim that by
11
+ * construction never appears. Deriving the flag from a comparison instead means
12
+ * one `setTimeout` per loading pulse, which is the floor for these semantics.
13
+ */
14
+ export declare function useDelayedFlag(active: boolean, delayMs: number): boolean;
1
15
  export default function LoadingOverlay({ statusMessage, progress, isVisible, immediate, canceled, onCancel, onRetry, }: {
2
16
  statusMessage?: string;
3
17
  progress?: number;
@@ -27,7 +27,7 @@ const flashDelayMs = 250;
27
27
  * construction never appears. Deriving the flag from a comparison instead means
28
28
  * one `setTimeout` per loading pulse, which is the floor for these semantics.
29
29
  */
30
- function useDelayedFlag(active, delayMs) {
30
+ export function useDelayedFlag(active, delayMs) {
31
31
  const [previous, setPrevious] = useState(active);
32
32
  const [activation, setActivation] = useState(0);
33
33
  if (previous !== active) {