@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
@@ -8,9 +8,9 @@ export { getFileName } from './getFileName.js';
8
8
  // it. `fetchContext.ts` takes it from mstUtils instead, which is what keeps the
9
9
  // configuration schemas this file pulls in out of the fetch harness's graph.
10
10
  export { getRpcSessionId } from './parentWalk.js';
11
- import { getParent, getSnapshot, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
11
+ import { getParent, getSnapshot, hasParent, isAlive, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
12
12
  import { observable, runInAction, untracked } from 'mobx';
13
- import { isConfigurationSlot, preProcessSlotValues, readConfObject, } from '../configuration/index.js';
13
+ import { readConfObject } from '../configuration/index.js';
14
14
  import { getFileHandle, storeFileHandle, verifyPermission, } from './fileHandleStore.js';
15
15
  import { getFileName } from './getFileName.js';
16
16
  import { getContainingView, getEnv, getSession, objectHash, } from './mstUtils.js';
@@ -76,12 +76,22 @@ export function isSameAssemblyName(a, b, assemblyManager) {
76
76
  * `ReferenceSequenceTrack` does not — its schema omits the slot deliberately
77
77
  * (see `createReferenceSeqTrackConfig`), because the assembly config holding it
78
78
  * IS its assembly, so the name is read off the parent instead.
79
+ *
80
+ * That makes a sequence track's assembly **positional**, so a copy of one that
81
+ * is not inside its assembly cannot answer at all — and a detached config is
82
+ * what every hydration path produces (`hydrateTrackConfig`, and the working
83
+ * copy a non-admin's edits would go to, ADR-032). `hasParent` is the guard:
84
+ * without it the walk raises MST's "Failed to find the parent" from a helper
85
+ * whose other two failure modes are a plain `undefined`.
79
86
  */
80
87
  function confAssemblyNames(conf) {
81
88
  const trackAssemblyNames = readConfObject(conf, 'assemblyNames');
82
89
  if (trackAssemblyNames) {
83
90
  return trackAssemblyNames;
84
91
  }
92
+ if (!hasParent(conf)) {
93
+ return undefined;
94
+ }
85
95
  const parent = getParent(conf);
86
96
  return 'sequence' in parent
87
97
  ? [readConfObject(parent, 'name')]
@@ -368,8 +378,12 @@ export function guessAdapter(file, index, adapterHint, model) {
368
378
  }
369
379
  export function guessTrackType(adapterType, model, file) {
370
380
  if (model) {
371
- const session = getSession(model);
372
- const trackTypeGuesser = getEnv(session).pluginManager.evaluateExtensionPoint(
381
+ // getEnv(model), not getEnv(getSession(model)): the env is tree-wide, so
382
+ // the session hop only narrowed what may be passed here — a session itself
383
+ // has no session ancestor, so guessAdapter above accepted one and this
384
+ // threw on it
385
+ const { pluginManager } = getEnv(model);
386
+ const trackTypeGuesser = pluginManager.evaluateExtensionPoint(
373
387
  /** #extensionPoint Core-guessTrackTypeForLocation | sync | Guess a track type from a file location */
374
388
  'Core-guessTrackTypeForLocation', () => undefined);
375
389
  const trackType = trackTypeGuesser(adapterType, file);
@@ -392,24 +406,35 @@ export function guessTrackType(adapterType, model, file) {
392
406
  */
393
407
  export function guessTrackConf(input, pluginManager, assemblyName) {
394
408
  const { uri, index, ...extra } = typeof input === 'string' ? { uri: input } : input;
395
- const file = { uri, locationType: 'UriLocation' };
396
- const indexLocation = index
397
- ? { uri: index, locationType: 'UriLocation' }
398
- : undefined;
409
+ return {
410
+ ...guessTrackConfForLocation({ uri, locationType: 'UriLocation' }, index ? { uri: index, locationType: 'UriLocation' } : undefined, pluginManager, assemblyName),
411
+ ...extra,
412
+ };
413
+ }
414
+ /**
415
+ * {@link guessTrackConf} over an already-built {@link FileLocation}, for a
416
+ * caller holding a location the loose `{ uri }` vocabulary cannot spell — a
417
+ * desktop local path. Kept separate so `LooseTrackInput` stays URI-only: it
418
+ * sits on the track-config union and the root config's `tracks`, where a
419
+ * local path would be a config a web deployment could not open.
420
+ *
421
+ * The `trackId` is a content hash of the adapter, not a timestamp, so adding
422
+ * the same file twice is idempotent — `addToSession` dedupes on it.
423
+ */
424
+ export function guessTrackConfForLocation(file, index, pluginManager, assemblyName) {
399
425
  const adapterGuesser = pluginManager.evaluateExtensionPoint('Core-guessAdapterForLocation', () => undefined);
400
- const adapter = adapterGuesser(file, indexLocation, undefined);
426
+ const adapter = adapterGuesser(file, index, undefined);
427
+ const name = getFileName(file);
401
428
  if (!adapter || adapter.type === UNKNOWN) {
402
- throw new Error(`could not infer a track type from "${uri}"; pass a full track config instead`);
429
+ throw new Error(`could not infer a track type from "${name}"; pass a full track config instead`);
403
430
  }
404
431
  const trackTypeGuesser = pluginManager.evaluateExtensionPoint('Core-guessTrackTypeForLocation', () => undefined);
405
- const name = getFileName(file);
406
432
  return {
407
433
  trackId: `${name}-${objectHash(adapter).slice(0, 8)}`,
408
434
  type: trackTypeGuesser(adapter.type, file) ?? 'FeatureTrack',
409
435
  name,
410
436
  assemblyNames: assemblyName ? [assemblyName] : [],
411
437
  adapter,
412
- ...extra,
413
438
  };
414
439
  }
415
440
  /**
@@ -572,6 +597,57 @@ export function normalizeTrackInit(t) {
572
597
  };
573
598
  }
574
599
  }
600
+ function resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf) {
601
+ const { pluginManager } = getEnv(self);
602
+ const session = getSession(self);
603
+ // `trackId` seeds the expansion because `guessTrackConf` otherwise
604
+ // synthesizes one from the filename, and the dedupe in the callers and
605
+ // `hideTrackGeneric` both look the track up by the id the caller passed.
606
+ // Only for the loose form: spreading a live MST node (SvInspector's
607
+ // circular track) would flatten it out of the `isStateTreeNode` branch.
608
+ const inlineRawConf = isLooseTrackConfig(inlineConf)
609
+ ? expandLooseTrackConfig({ trackId, ...inlineConf }, pluginManager)
610
+ : inlineConf;
611
+ const rawConf = inlineRawConf ?? session.getTrackById(trackId);
612
+ if (!rawConf) {
613
+ throw new Error(`Could not resolve identifier "${trackId}"`);
614
+ }
615
+ const confSnapshot = structuredClone(isStateTreeNode(rawConf) ? getSnapshot(rawConf) : rawConf);
616
+ const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot);
617
+ const trackType = pluginManager.getTrackType(conf.type);
618
+ // Validate only a conf that is not already in the tree. `configSchema.create`
619
+ // here builds an entire config node and throws it away purely to surface a
620
+ // nice error; its preProcessSnapshot also re-runs Core-preProcessTrackConfig
621
+ // on a snapshot this function just preprocessed. A conf that is a state tree
622
+ // node was validated when MST created it, so all of that is pure waste on
623
+ // the common showTrack path.
624
+ if (!isStateTreeNode(rawConf)) {
625
+ try {
626
+ trackType.configSchema.create(conf, getEnv(self));
627
+ }
628
+ catch (e) {
629
+ throw new Error(`Track "${trackId}" has an invalid configuration: ${e}`, {
630
+ cause: e,
631
+ });
632
+ }
633
+ }
634
+ // A track container that isn't itself a view — a synteny level, which owns
635
+ // a track list but has no width and no registered view type — takes the
636
+ // display choice from the view it sits in, which is the same node
637
+ // getContainingView already resolves to for everything else beneath it.
638
+ const view = isViewModel(self) ? self : getContainingView(self);
639
+ const viewType = pluginManager.getViewType(view.type);
640
+ const picked = pickDisplayForView({
641
+ declaredDisplays: conf.displays ?? [],
642
+ requestedType: displayInitialSnapshot.type,
643
+ trackDisplayTypes: trackType.displayTypes.map(d => d.name),
644
+ viewDisplayTypes: viewType.displayTypes.map(d => d.name),
645
+ });
646
+ if (!picked) {
647
+ throw new Error(`Could not find a compatible display for view type ${view.type}`);
648
+ }
649
+ return { rawConf, conf, trackType, picked };
650
+ }
575
651
  export function showTrackGeneric(self, trackId,
576
652
  // `object`, annotated, rather than inferred from the default. A bare `{}`
577
653
  // accepts every non-nullish value, numbers included, which is how a synteny
@@ -597,49 +673,28 @@ inlineConf) {
597
673
  // snackbars. Config is validated before the push so the open set never holds
598
674
  // a broken track.
599
675
  try {
600
- const rawConf = (inlineConf && expandLooseTrackConfig(inlineConf, pluginManager)) ??
601
- session.getTrackById(trackId);
602
- if (!rawConf) {
603
- throw new Error(`Could not resolve identifier "${trackId}"`);
604
- }
605
- const confSnapshot = structuredClone(isStateTreeNode(rawConf) ? getSnapshot(rawConf) : rawConf);
606
- const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot);
607
- const trackType = pluginManager.getTrackType(conf.type);
608
- // Validate only a conf that is not already in the tree. `configSchema.create`
609
- // here builds an entire config node and throws it away purely to surface a
610
- // nice error; its preProcessSnapshot also re-runs Core-preProcessTrackConfig
611
- // on a snapshot this function just preprocessed. A conf that is a state tree
612
- // node was validated when MST created it, so all of that is pure waste on
613
- // the common showTrack path.
614
- if (!isStateTreeNode(rawConf)) {
615
- try {
616
- trackType.configSchema.create(conf, getEnv(self));
617
- }
618
- catch (e) {
619
- throw new Error(`Track "${trackId}" has an invalid configuration: ${e}`, { cause: e });
620
- }
621
- }
622
- // A track container that isn't itself a view — a synteny level, which owns
623
- // a track list but has no width and no registered view type — takes the
624
- // display choice from the view it sits in, which is the same node
625
- // getContainingView already resolves to for everything else beneath it.
626
- const view = isViewModel(self) ? self : getContainingView(self);
627
- const viewType = pluginManager.getViewType(view.type);
628
- const picked = pickDisplayForView({
629
- declaredDisplays: conf.displays ?? [],
630
- requestedType: displayInitialSnapshot.type,
631
- trackDisplayTypes: trackType.displayTypes.map(d => d.name),
632
- viewDisplayTypes: viewType.displayTypes.map(d => d.name),
633
- });
634
- if (!picked) {
635
- throw new Error(`Could not find a compatible display for view type ${view.type}`);
636
- }
676
+ const { rawConf, conf, trackType, picked } = resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf);
637
677
  const { type: displayType, conf: displayConf } = picked;
638
678
  const displayId = displayConf?.displayId ?? `${trackId}-${displayType}`;
679
+ // a registered display whose model is not loaded yet: the load re-enters
680
+ // through the view's own showTrack action, so a synchronous caller gets the
681
+ // track a tick late; a caller that needs it back awaits launchTrack
682
+ const displayRecord = pluginManager.resolveDisplayTypeRecord(displayType);
683
+ if (displayRecord && !displayRecord.isStateModelLoaded) {
684
+ void launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
685
+ return undefined;
686
+ }
639
687
  const track = trackType.stateModel.create({
640
688
  ...initialSnapshot,
641
689
  type: conf.type,
642
- configuration: inlineConf ?? trackId,
690
+ // `rawConf`, not the caller's `inlineConf`: the loose `{ trackId, uri }`
691
+ // form has no `type` for the config schema to dispatch on, and the
692
+ // concrete schema this lands on carries no loose preprocessor — that one
693
+ // is on `pluggableConfigSchemaType('track')`, which a track model's
694
+ // `ConfigurationReference` is not. Not `conf` either: that has already
695
+ // run Core-preProcessTrackConfig, which the schema's own
696
+ // preProcessSnapshot runs again.
697
+ configuration: inlineConf ? rawConf : trackId,
643
698
  displays: [
644
699
  {
645
700
  ...displayConf,
@@ -652,23 +707,30 @@ inlineConf) {
652
707
  self.tracks.push(track);
653
708
  // Display settings (height, color, …) are config slots now, not display
654
709
  // instance props — passed in the display snapshot they'd be dropped as
655
- // unknown MST keys. Route the ones that are real slots onto the persistent
656
- // display config so they take effect and survive hide/retick (#5591). Runs
657
- // after the push so the display's config reference can resolve.
710
+ // unknown MST keys. applyDisplaySettings routes the ones that are real
711
+ // slots onto the persistent display config so they take effect and
712
+ // survive hide/retick (#5591), running the schema's preProcessSnapshot
713
+ // first so shorthand expansions and legacy-key migrations reach this path
714
+ // too — a session spec, share link, or embed writes slots here rather
715
+ // than creating a config from a snapshot, and those surfaces are meant to
716
+ // speak the same vocabulary. Runs after the push so the display's config
717
+ // reference can resolve. Slots only, deliberately: this is the
718
+ // declarative surface, and `allowSetters` stays off so a spec key cannot
719
+ // reach internal display actions.
658
720
  //
659
- // preProcessSlotValues first, so a display schema's shorthand expansions
660
- // and legacy-key migrations reach this path too: a session spec, share
661
- // link, or embed writes slots here rather than creating a config from a
662
- // snapshot, and those two surfaces are meant to speak the same vocabulary.
663
- const display = track.displays[0];
664
- const displaySlots = preProcessSlotValues(display.configuration, displayInitialSnapshot);
665
- for (const [key, value] of Object.entries(displaySlots)) {
666
- if (key !== 'type' && isConfigurationSlot(display.configuration, key)) {
667
- // the key comes from a snapshot at runtime, and setConf's slot name is
668
- // a compile-time type
669
- // eslint-disable-next-line no-restricted-syntax
670
- display.configuration.setSlot(key, value);
671
- }
721
+ // `type` is dropped first because this function already consumed it
722
+ // (pickDisplayForView above), and only `failed` — a key whose write threw —
723
+ // is notified: `unapplied` here also holds the MST display props the
724
+ // snapshot spread above already applied, so notifying on it would report a
725
+ // correct call as broken. A throwing slot value used to reach the outer
726
+ // catch and raise a snackbar; per-key catching kept the track intact but
727
+ // took the message away with it.
728
+ const displaySettings = Object.fromEntries(Object.entries(displayInitialSnapshot).filter(([key]) => key !== 'type'));
729
+ const report = track.applyDisplaySettings(displaySettings);
730
+ if (report.failed.length) {
731
+ session.notifyError(`Track "${trackId}" rejected ${report.failed
732
+ .map(f => `${f.key}: ${f.error}`)
733
+ .join('; ')}`);
672
734
  }
673
735
  // if this track came from a connection, persist its config so it survives
674
736
  // reload without re-establishing the connection (no-op otherwise)
@@ -708,6 +770,32 @@ export function toggleTrackGeneric(self, trackId) {
708
770
  ? false
709
771
  : !!showTrackGeneric(self, trackId);
710
772
  }
773
+ export async function launchTrackGeneric(self, trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
774
+ const { pluginManager } = getEnv(self);
775
+ const session = getSession(self);
776
+ const found = self.tracks.find(t => t.configuration.trackId === trackId);
777
+ if (found) {
778
+ return found;
779
+ }
780
+ try {
781
+ const { picked } = resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf);
782
+ await pluginManager.resolveDisplayTypeRecord(picked.type)?.loadStateModel();
783
+ }
784
+ catch (e) {
785
+ session.notifyError(`${e}`, e);
786
+ return undefined;
787
+ }
788
+ if (!isAlive(self)) {
789
+ return undefined;
790
+ }
791
+ return self.showTrack?.(trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
792
+ }
793
+ // the hide runs in the synchronous prologue, so it keeps action context
794
+ export async function launchToggleTrackGeneric(self, trackId) {
795
+ return hideTrackGeneric(self, trackId)
796
+ ? false
797
+ : !!(await launchTrackGeneric(self, trackId));
798
+ }
711
799
  /**
712
800
  * Every track config the session can show, connection-supplied ones included.
713
801
  *
@@ -726,3 +814,41 @@ export function allSessionTracks(session) {
726
814
  ? [...session.tracks, ...connectionTracks]
727
815
  : session.tracks;
728
816
  }
817
+ /**
818
+ * The session's annotation for `assemblyName`, as trackIds: what a view opened
819
+ * on another genome starts with. Only FeatureTracks, not everything configured
820
+ * for the assembly — an alignments track turned on behind the user's back
821
+ * fetches data nobody asked for, and a FeatureTrack over a few kb is one tabix
822
+ * range query.
823
+ */
824
+ export function annotationTrackIds(session, assemblyName) {
825
+ return allSessionTracks(session).flatMap(track => readConfObject(track, 'type') === 'FeatureTrack' &&
826
+ readConfObject(track, 'assemblyNames').some(name => isSameAssemblyName(name, assemblyName, session.assemblyManager))
827
+ ? [readConfObject(track, 'trackId')]
828
+ : []);
829
+ }
830
+ function isNavigableView(view) {
831
+ return 'navToLocString' in view;
832
+ }
833
+ /**
834
+ * One assembly on its own in a linear genome view, at `loc`. Keyed on `id`:
835
+ * following several alignments to one genome re-navigates the view rather than
836
+ * stacking a new one each time.
837
+ */
838
+ export async function openAssemblyInLinearView({ session, id, assemblyName, loc, tracks, }) {
839
+ const view = session.views.find(v => v.id === id);
840
+ if (view && isNavigableView(view)) {
841
+ await view.navToLocString(loc, assemblyName);
842
+ }
843
+ else {
844
+ session.addView('LinearGenomeView', {
845
+ id,
846
+ assembly: assemblyName,
847
+ loc,
848
+ tracks,
849
+ });
850
+ }
851
+ }
852
+ export function openMateLabel(assemblyName) {
853
+ return `Open ${assemblyName} at the matching region`;
854
+ }
@@ -1,3 +1,4 @@
1
+ import type { ViewSnapshotInput } from '../../PluginManager.ts';
1
2
  import type TextSearchManager from '../../TextSearch/TextSearchManager.ts';
2
3
  import type { AnyConfigurationModel, ResolvableDisplay } from '../../configuration/index.ts';
3
4
  import type { BaseInternetAccountModel } from '../../pluggableElementTypes/models/index.ts';
@@ -22,7 +23,8 @@ export type { AugmentedRegion, BasePlugin, BlobLocation, FileHandleLocation, Fil
22
23
  export interface AbstractViewContainer extends IStateTreeNode {
23
24
  views: AbstractViewModel[];
24
25
  removeView(view: AbstractViewModel): void;
25
- addView(typeName: string, initialState?: Record<string, unknown>): AbstractViewModel;
26
+ addView<N extends string>(typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): AbstractViewModel;
27
+ launchView(typeName: string, initialState?: Record<string, unknown>): Promise<AbstractViewModel>;
26
28
  }
27
29
  export declare function isViewContainer(thing: unknown): thing is AbstractViewContainer;
28
30
  /**
@@ -32,6 +34,7 @@ export declare function isViewContainer(thing: unknown): thing is AbstractViewCo
32
34
  */
33
35
  export interface TrackActionView {
34
36
  showTrack: (id: string) => void;
37
+ launchTrack: (id: string) => Promise<unknown>;
35
38
  getActiveDisplayId?: (trackId: string) => string | undefined;
36
39
  }
37
40
  /**
@@ -153,6 +156,21 @@ export interface SessionWithConfigEditing extends AbstractSessionModel {
153
156
  }): void;
154
157
  }
155
158
  export declare function isSessionModelWithConfigEditing(t: unknown): t is SessionWithConfigEditing;
159
+ /**
160
+ * abstract interface for a session that keeps a list of views, so that `addView`
161
+ * appends and the view a launch came out of survives it.
162
+ *
163
+ * `AbstractViewContainer` alone does not say this. The single-view embedded
164
+ * products satisfy it with a `views` getter that wraps their one `view` in a
165
+ * fresh plain array and an `addView` that overwrites that view, so "Open in new
166
+ * view" there would destroy the view it was offered from. A view list that is
167
+ * itself part of the state tree is what tells the two apart: only a session
168
+ * that stores its views can hold one more.
169
+ */
170
+ export interface SessionWithMultipleViews extends AbstractSessionModel {
171
+ views: AbstractViewModel[] & IStateTreeNode;
172
+ }
173
+ export declare function isSessionWithMultipleViews(t: unknown): t is SessionWithMultipleViews;
156
174
  /**
157
175
  * abstract interface for a session that can swap one of its views for a new one
158
176
  * of another type, in the slot the old view occupied.
@@ -164,7 +182,7 @@ export declare function isSessionModelWithConfigEditing(t: unknown): t is Sessio
164
182
  * and the guard is what a launcher asks before offering it.
165
183
  */
166
184
  export interface SessionWithViewReplacement extends AbstractSessionModel {
167
- replaceView(view: AbstractViewModel, typeName: string, initialState?: Record<string, unknown>): AbstractViewModel;
185
+ replaceView<N extends string>(view: AbstractViewModel, typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): AbstractViewModel;
168
186
  }
169
187
  export declare function isSessionWithViewReplacement(t: unknown): t is SessionWithViewReplacement;
170
188
  /**
@@ -188,10 +206,10 @@ export declare function canReplaceView(session: AbstractViewContainer, view: Abs
188
206
  * that can't replace a view falls back to appending, so a caller passes the
189
207
  * source view unconditionally and never has to ask twice.
190
208
  */
191
- export declare function addOrReplaceView({ session, typeName, initialState, replacing, }: {
209
+ export declare function addOrReplaceView<N extends string>({ session, typeName, initialState, replacing, }: {
192
210
  session: AbstractViewContainer;
193
- typeName: string;
194
- initialState?: Record<string, unknown>;
211
+ typeName: N;
212
+ initialState?: NoInfer<ViewSnapshotInput<N>>;
195
213
  replacing?: AbstractViewModel;
196
214
  }): AbstractViewModel;
197
215
  /**
@@ -371,6 +389,7 @@ export declare function isSessionWithSessionPlugins(thing: unknown): thing is Se
371
389
  */
372
390
  export interface SessionWithPermanentPlugins extends AbstractSessionModel {
373
391
  permanentPlugins: PluginDefinition[];
392
+ permanentPluginsSkipped: boolean;
374
393
  addPermanentPlugin: (plugin: PluginDefinition) => void;
375
394
  removePermanentPlugin: (plugin: PluginDefinition) => void;
376
395
  }
@@ -410,6 +429,13 @@ export interface TrackContainer {
410
429
  * silence.
411
430
  */
412
431
  showTrack: (trackId: string, initialSnapshot?: object, displayInitialSnapshot?: Record<string, unknown>, inlineConf?: Record<string, unknown>) => unknown;
432
+ /**
433
+ * The sync pair still works on an unloaded display — it loads and shows a
434
+ * tick later, returning undefined — so these are for a caller that needs
435
+ * the track back.
436
+ */
437
+ launchTrack: (trackId: string, initialSnapshot?: object, displayInitialSnapshot?: Record<string, unknown>, inlineConf?: Record<string, unknown>) => Promise<unknown>;
438
+ launchToggleTrack: (trackId: string) => Promise<unknown>;
413
439
  hideTrack: (trackId: string) => unknown;
414
440
  toggleTrack: (trackId: string) => unknown;
415
441
  }
@@ -453,6 +479,23 @@ export interface AbstractViewModel {
453
479
  * `trackContainerFor` cannot answer that — a walker has no id to ask with.
454
480
  */
455
481
  trackContainers?: TrackContainer[];
482
+ /**
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.
488
+ */
489
+ 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[];
456
499
  }
457
500
  export declare function isViewModel(thing: unknown): thing is AbstractViewModel;
458
501
  type Display = {
@@ -18,6 +18,9 @@ export function isSessionModel(thing) {
18
18
  export function isSessionModelWithConfigEditing(t) {
19
19
  return isSessionModel(t) && 'editConfiguration' in t;
20
20
  }
21
+ export function isSessionWithMultipleViews(t) {
22
+ return isSessionModel(t) && isStateTreeNode(t.views);
23
+ }
21
24
  export function isSessionWithViewReplacement(t) {
22
25
  return isSessionModel(t) && 'replaceView' in t;
23
26
  }
@@ -0,0 +1,36 @@
1
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ /** what a diagnostic calls the view: its snapshot `type`, or the model name */
3
+ export declare function viewLabel(self: IStateTreeNode): string;
4
+ /**
5
+ * What every surface calls this mistake. A session spec launches a view without
6
+ * ever building a snapshot, so it classifies its own keys and reports them
7
+ * through its own session rather than through the sink below — one wording, so
8
+ * a typo reads the same whether it was written in a config or in a URL.
9
+ */
10
+ export declare function unknownKeysMessage(label: string, keys: string[]): string;
11
+ /**
12
+ * Name the keys a view was handed and could not place, from the partition
13
+ * `withLaunchInput` runs on the snapshot.
14
+ */
15
+ export declare function reportUnknownKeys(self: IStateTreeNode, keys: string[]): void;
16
+ /**
17
+ * Name the row lists a view refused whole. `withLaunchInput` splits an authored
18
+ * array into the recipes a launcher resolves and the built snapshots MST
19
+ * restores, per entry — but a row list is indexed against the view's `levels`
20
+ * and per-level tracks, so a list holding both kinds has no correct split and
21
+ * is refused rather than renumbered.
22
+ */
23
+ export declare function reportMalformedRows(self: IStateTreeNode, keys: string[]): void;
24
+ /**
25
+ * What every surface calls v4's nested `init`. v5 takes every setting directly
26
+ * on the view object and unwraps the nesting on the way in, so the settings
27
+ * still arrive — this names the spelling to fix while they do.
28
+ */
29
+ export declare function legacyInitMessage(label: string): string;
30
+ /**
31
+ * Name a snapshot that still writes v4's nested `init`. The console alone,
32
+ * unlike the two reporters above: the settings were applied, so there is
33
+ * nothing for a visitor to act on and a warning toast over a working view is
34
+ * noise. `jbrowse validate` is where the author of the config hears about it.
35
+ */
36
+ export declare function reportLegacyInit(self: IStateTreeNode): void;
@@ -0,0 +1,69 @@
1
+ import { getType } from '@jbrowse/mobx-state-tree';
2
+ import { getNotificationSink } from './sessionServices.js';
3
+ /** what a diagnostic calls the view: its snapshot `type`, or the model name */
4
+ export function viewLabel(self) {
5
+ return self.type ?? getType(self).name;
6
+ }
7
+ /**
8
+ * What every surface calls this mistake. A session spec launches a view without
9
+ * ever building a snapshot, so it classifies its own keys and reports them
10
+ * through its own session rather than through the sink below — one wording, so
11
+ * a typo reads the same whether it was written in a config or in a URL.
12
+ */
13
+ export function unknownKeysMessage(label, keys) {
14
+ return `${label} ignored unknown key(s): ${keys.join(', ')}`;
15
+ }
16
+ /**
17
+ * Name the keys a view was handed and could not place, from the partition
18
+ * `withLaunchInput` runs on the snapshot.
19
+ */
20
+ export function reportUnknownKeys(self, keys) {
21
+ if (!keys.length) {
22
+ return;
23
+ }
24
+ const message = unknownKeysMessage(viewLabel(self), keys);
25
+ console.warn(message);
26
+ try {
27
+ getNotificationSink(self).notify(message, 'warning');
28
+ }
29
+ catch {
30
+ // a view built outside a session has nowhere to put it
31
+ }
32
+ }
33
+ /**
34
+ * Name the row lists a view refused whole. `withLaunchInput` splits an authored
35
+ * array into the recipes a launcher resolves and the built snapshots MST
36
+ * restores, per entry — but a row list is indexed against the view's `levels`
37
+ * and per-level tracks, so a list holding both kinds has no correct split and
38
+ * is refused rather than renumbered.
39
+ */
40
+ export function reportMalformedRows(self, keys) {
41
+ if (!keys.length) {
42
+ return;
43
+ }
44
+ const message = `${viewLabel(self)} refused ${keys.join(', ')}: the list mixes built view snapshots with recipes to open one, and the rows index against the levels between them. Write all of them one way.`;
45
+ console.warn(message);
46
+ try {
47
+ getNotificationSink(self).notify(message, 'warning');
48
+ }
49
+ catch {
50
+ // a view built outside a session has nowhere to put it
51
+ }
52
+ }
53
+ /**
54
+ * What every surface calls v4's nested `init`. v5 takes every setting directly
55
+ * on the view object and unwraps the nesting on the way in, so the settings
56
+ * still arrive — this names the spelling to fix while they do.
57
+ */
58
+ export function legacyInitMessage(label) {
59
+ return `${label} nests its settings under "init", which is deprecated: write every setting directly on the view object.`;
60
+ }
61
+ /**
62
+ * Name a snapshot that still writes v4's nested `init`. The console alone,
63
+ * unlike the two reporters above: the settings were applied, so there is
64
+ * nothing for a visitor to act on and a warning toast over a working view is
65
+ * noise. `jbrowse validate` is where the author of the config hears about it.
66
+ */
67
+ export function reportLegacyInit(self) {
68
+ console.warn(legacyInitMessage(viewLabel(self)));
69
+ }
@@ -56,3 +56,26 @@ export interface ViewStatusInputs {
56
56
  * with nothing anywhere saying why. `noRegions` is that state, named.
57
57
  */
58
58
  export declare function computeViewStatus({ error, hasSomethingToShow, loading, }: ViewStatusInputs): ViewStatus;
59
+ /**
60
+ * The slice of `AssemblyManager` this needs, spelled out rather than imported:
61
+ * the real type is an MST model a `PluginManager` built, so naming it here
62
+ * would cost this leaf the whole application's type graph (`util/CLAUDE.md`).
63
+ */
64
+ interface AssemblyErrorSource {
65
+ get: (name: string) => {
66
+ error?: unknown;
67
+ } | undefined;
68
+ }
69
+ /**
70
+ * The assembly failures behind a view's `assemblyNames`, joined, or `undefined`
71
+ * when there are none.
72
+ *
73
+ * **Undefined and not `''`**, which is the whole reason this is shared. Three
74
+ * views spelled it as a bare `.join(', ')`, and a getter that answers the empty
75
+ * string for "nothing failed" reads as an error to every `!== undefined` test —
76
+ * `DotplotView.error` folded it in with `??`, so the view reported a terminal
77
+ * error permanently and jbrowse-img refused to render any dotplot. The in-app
78
+ * readers all happened to be truthiness tests, which is why it stayed invisible.
79
+ */
80
+ export declare function assemblyErrorMessage(assemblyManager: AssemblyErrorSource, assemblyNames: string[]): string | undefined;
81
+ export {};
@@ -18,3 +18,20 @@ export function computeViewStatus({ error, hasSomethingToShow, loading, }) {
18
18
  const pending = loading();
19
19
  return pending ? { type: 'loading', ...pending } : { type: 'ready' };
20
20
  }
21
+ /**
22
+ * The assembly failures behind a view's `assemblyNames`, joined, or `undefined`
23
+ * when there are none.
24
+ *
25
+ * **Undefined and not `''`**, which is the whole reason this is shared. Three
26
+ * views spelled it as a bare `.join(', ')`, and a getter that answers the empty
27
+ * string for "nothing failed" reads as an error to every `!== undefined` test —
28
+ * `DotplotView.error` folded it in with `??`, so the view reported a terminal
29
+ * error permanently and jbrowse-img refused to render any dotplot. The in-app
30
+ * readers all happened to be truthiness tests, which is why it stayed invisible.
31
+ */
32
+ export function assemblyErrorMessage(assemblyManager, assemblyNames) {
33
+ const errors = assemblyNames
34
+ .map(name => assemblyManager.get(name)?.error)
35
+ .filter(e => !!e);
36
+ return errors.length > 0 ? errors.join(', ') : undefined;
37
+ }