@jbrowse/core 5.0.0-beta.11 → 5.0.0-beta.13

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 (99) hide show
  1. package/README.md +97 -75
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.d.ts +3 -0
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/Attributes.js +14 -1
  4. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.d.ts +2 -0
  5. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeatureDetails.js +5 -5
  6. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.js +1 -1
  7. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.d.ts +3 -1
  8. package/esm/BaseFeatureWidget/SequenceFeatureDetails/dialogs/FeatureSequenceDialog.js +2 -2
  9. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.d.ts +2 -1
  10. package/esm/BaseFeatureWidget/SequenceFeatureDetails/featureTypeUtil.js +7 -14
  11. package/esm/BaseFeatureWidget/SequenceFeatureDetails/seqtypes/CDNASequence.js +3 -3
  12. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +7 -0
  13. package/esm/BaseFeatureWidget/stateModelFactory.js +11 -0
  14. package/esm/BaseFeatureWidget/types.d.ts +2 -0
  15. package/esm/PluginManager.d.ts +6 -3
  16. package/esm/PluginManager.js +6 -3
  17. package/esm/ReExports/coreModules.generated.js +291 -285
  18. package/esm/ReExports/coreWorkerModules.generated.js +288 -283
  19. package/esm/ReExports/list.js +7 -0
  20. package/esm/TextSearch/TextSearchManager.d.ts +3 -3
  21. package/esm/TextSearch/TextSearchManager.js +6 -13
  22. package/esm/configuration/configurationSchema.d.ts +33 -95
  23. package/esm/configuration/configurationSchema.js +50 -218
  24. package/esm/configuration/configurationSlot.d.ts +18 -64
  25. package/esm/configuration/configurationSlot.js +31 -97
  26. package/esm/configuration/formatDetailsConfigSchema.js +2 -0
  27. package/esm/configuration/getConf.d.ts +7 -36
  28. package/esm/configuration/getConf.js +7 -37
  29. package/esm/configuration/index.d.ts +3 -2
  30. package/esm/configuration/index.js +2 -1
  31. package/esm/configuration/partitionAdvanced.d.ts +12 -0
  32. package/esm/configuration/partitionAdvanced.js +42 -0
  33. package/esm/configuration/plot.d.ts +6 -0
  34. package/esm/configuration/plot.js +34 -1
  35. package/esm/configuration/readConfObject.d.ts +15 -17
  36. package/esm/configuration/readConfObject.js +19 -50
  37. package/esm/configuration/schemaTypes.js +5 -1
  38. package/esm/configuration/snapshotPreprocess.js +1 -3
  39. package/esm/configuration/types.d.ts +60 -159
  40. package/esm/pluggableElementTypes/DisplayType.d.ts +7 -0
  41. package/esm/pluggableElementTypes/DisplayType.js +7 -0
  42. package/esm/pluggableElementTypes/RpcMethodType.d.ts +7 -0
  43. package/esm/pluggableElementTypes/RpcMethodType.js +9 -0
  44. package/esm/pluggableElementTypes/addMenuItems.d.ts +2 -1
  45. package/esm/pluggableElementTypes/addMenuItems.js +2 -1
  46. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +3 -2
  47. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +5 -5
  48. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +2 -4
  49. package/esm/pluggableElementTypes/models/BaseTrackModel.js +2 -4
  50. package/esm/pluggableElementTypes/models/baseTrackConfig.js +3 -0
  51. package/esm/rpc/RpcRegistry.d.ts +2 -1
  52. package/esm/rpc/byteBudget.d.ts +7 -0
  53. package/esm/rpc/byteBudget.js +10 -0
  54. package/esm/rpc/coreRpcMethods.d.ts +1 -1
  55. package/esm/rpc/coreRpcMethods.js +1 -1
  56. package/esm/rpc/explainTransferError.js +3 -3
  57. package/esm/rpc/methods/CoreLoadTrackCode.d.ts +17 -0
  58. package/esm/rpc/methods/CoreLoadTrackCode.js +24 -0
  59. package/esm/ui/AppLogo.js +43 -3
  60. package/esm/ui/buildExtraTrackMenuItems.d.ts +2 -2
  61. package/esm/ui/colors.d.ts +41 -30
  62. package/esm/ui/colors.js +81 -44
  63. package/esm/ui/multiTrackMenuItems.d.ts +2 -2
  64. package/esm/util/alignedBytes.d.ts +19 -0
  65. package/esm/util/alignedBytes.js +37 -0
  66. package/esm/util/analytics.d.ts +5 -6
  67. package/esm/util/analytics.js +5 -3
  68. package/esm/util/bezierConnector.js +5 -1
  69. package/esm/util/categoricalField.d.ts +23 -3
  70. package/esm/util/categoricalField.js +36 -3
  71. package/esm/util/cellMatches.js +14 -14
  72. package/esm/util/cellsStep.d.ts +3 -5
  73. package/esm/util/cellsStep.js +31 -29
  74. package/esm/util/color-bits/parse.js +22 -11
  75. package/esm/util/colorRamp.js +6 -0
  76. package/esm/util/colorScale.d.ts +9 -2
  77. package/esm/util/colorScale.js +33 -2
  78. package/esm/util/colorSchemes.d.ts +4 -2
  79. package/esm/util/colorSchemes.js +4 -1
  80. package/esm/util/isoformRank.d.ts +39 -0
  81. package/esm/util/isoformRank.js +118 -0
  82. package/esm/util/markEncodingTypes.d.ts +3 -1
  83. package/esm/util/quantileExtent.d.ts +22 -0
  84. package/esm/util/quantileExtent.js +24 -1
  85. package/esm/util/systemColorScheme.d.ts +4 -0
  86. package/esm/util/systemColorScheme.js +21 -0
  87. package/esm/util/tabix.d.ts +1 -1
  88. package/esm/util/tabix.js +1 -1
  89. package/esm/util/temporaryAssembly.d.ts +2 -2
  90. package/esm/util/temporaryAssembly.js +2 -3
  91. package/esm/util/trackConfigDelta.d.ts +1 -1
  92. package/esm/util/tracks.d.ts +25 -15
  93. package/esm/util/tracks.js +67 -43
  94. package/esm/util/types/index.d.ts +15 -7
  95. package/esm/util/types/index.js +21 -2
  96. package/esm/util/useRowVirtualScroll.d.ts +1 -0
  97. package/package.json +7 -4
  98. package/esm/rpc/methods/CoreLoadAdapterCode.d.ts +0 -13
  99. package/esm/rpc/methods/CoreLoadAdapterCode.js +0 -16
@@ -0,0 +1,118 @@
1
+ export const featureIsoformReader = {
2
+ type: f => f.get('type'),
3
+ start: f => f.get('start'),
4
+ end: f => f.get('end'),
5
+ subfeatures: f => f.get('subfeatures') ?? [],
6
+ attribute: (f, field) => f.get(field),
7
+ };
8
+ export const serializedIsoformReader = {
9
+ type: f => f.type,
10
+ start: f => f.start,
11
+ end: f => f.end,
12
+ subfeatures: f => f.subfeatures ?? [],
13
+ attribute: (f, field) => f[field],
14
+ };
15
+ export const DEFAULT_CANONICAL_TRANSCRIPTS = {
16
+ field: 'tag',
17
+ tags: [
18
+ 'MANE Select',
19
+ 'MANE_Select',
20
+ 'RefSeq Select',
21
+ 'Ensembl_canonical',
22
+ 'MANE Plus Clinical',
23
+ 'MANE_Plus_Clinical',
24
+ ],
25
+ };
26
+ function isCDSType(type) {
27
+ return type?.toLowerCase() === 'cds';
28
+ }
29
+ // The feature itself counts, because an isoform can BE the CDS: a viral
30
+ // polyprotein hangs its cleavage products off its CDS, not off further CDSs.
31
+ export function isCodingIsoform(read, feature) {
32
+ return (isCDSType(read.type(feature)) ||
33
+ read.subfeatures(feature).some(sub => isCodingIsoform(read, sub)));
34
+ }
35
+ // "Longest coding" is the longest protein — summed CDS length, not the widest
36
+ // genomic footprint an isoform with a large intron could win. A duplicated CDS
37
+ // row is a real GFF3 quirk that would otherwise inflate one isoform past a
38
+ // genuinely longer protein, so equal spans count once.
39
+ function codingLength(read, feature) {
40
+ const spans = [];
41
+ const walk = (f) => {
42
+ for (const sub of read.subfeatures(f)) {
43
+ if (isCDSType(read.type(sub))) {
44
+ spans.push([read.start(sub), read.end(sub)]);
45
+ }
46
+ else {
47
+ walk(sub);
48
+ }
49
+ }
50
+ };
51
+ walk(feature);
52
+ if (spans.length === 0) {
53
+ return isCDSType(read.type(feature))
54
+ ? read.end(feature) - read.start(feature)
55
+ : 0;
56
+ }
57
+ spans.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
58
+ let sum = 0;
59
+ for (const [i, span] of spans.entries()) {
60
+ const prev = spans[i - 1];
61
+ if (!prev || prev[0] !== span[0] || prev[1] !== span[1]) {
62
+ sum += span[1] - span[0];
63
+ }
64
+ }
65
+ return sum;
66
+ }
67
+ // A position in the tag list rather than a boolean, because the default list
68
+ // holds two tags one gene can carry at once and flattening them to "tagged"
69
+ // leaves the coding-length tiebreak to pick between them. A GFF3 attribute
70
+ // holding a comma list arrives as an array, hence both shapes.
71
+ function canonicalRank(value, wanted) {
72
+ const values = Array.isArray(value)
73
+ ? value.map(v => String(v).toLowerCase())
74
+ : typeof value === 'string'
75
+ ? [value.toLowerCase()]
76
+ : [];
77
+ let best = Infinity;
78
+ for (const v of values) {
79
+ const rank = wanted.indexOf(v);
80
+ if (rank !== -1 && rank < best) {
81
+ best = rank;
82
+ }
83
+ }
84
+ return best;
85
+ }
86
+ export function isoformScorer(read, { field, tags }) {
87
+ const wanted = tags.map(t => t.toLowerCase());
88
+ return (feature) => {
89
+ const coding = isCodingIsoform(read, feature);
90
+ return {
91
+ canonical: wanted.length
92
+ ? canonicalRank(read.attribute(feature, field), wanted)
93
+ : Infinity,
94
+ coding,
95
+ size: coding
96
+ ? codingLength(read, feature)
97
+ : read.end(feature) - read.start(feature),
98
+ };
99
+ };
100
+ }
101
+ // Best first. A curated tag outranks every measurement, because for a gene whose
102
+ // longest protein is a minor variant it is the only thing that picks the right
103
+ // isoform. A coding-length tie resolves to the LATER isoform, which a stable
104
+ // sort would break the other way — hence the explicit index term.
105
+ export function rankIsoforms(isoforms, scoreOf) {
106
+ return isoforms
107
+ .map((feature, index) => ({ feature, index, ...scoreOf(feature) }))
108
+ .sort((a, b) => a.canonical - b.canonical ||
109
+ Number(b.coding) - Number(a.coding) ||
110
+ b.size - a.size ||
111
+ b.index - a.index)
112
+ .map(s => s.feature);
113
+ }
114
+ export function isCanonicalTranscriptsHost(thing) {
115
+ return (typeof thing === 'object' &&
116
+ thing !== null &&
117
+ 'canonicalTranscripts' in thing);
118
+ }
@@ -463,7 +463,9 @@ export interface AggregateStep {
463
463
  * #api
464
464
  * Replace the features with runs of constant depth: how many of them overlap
465
465
  * each stretch of the region, in a field `as` (`coverage` by default), with
466
- * the stretches nothing overlaps left out.
466
+ * the stretches nothing overlaps left out. A read counts across its whole
467
+ * span, deletions and introns included; gap-aware depth stays the alignments
468
+ * display's (ADR-118).
467
469
  */
468
470
  export interface CoverageStep {
469
471
  type: 'coverage';
@@ -1,5 +1,7 @@
1
1
  /** The quantile "Clip outliers" clips at on a scale whose own default is the extremes. */
2
2
  export declare const DEFAULT_CLIP_QUANTILE = 0.99;
3
+ /** The lowest `domainQuantile` a scale reads, where its two ends meet; one below reads as this. */
4
+ export declare const MIN_DOMAIN_QUANTILE = 0.5;
3
5
  /**
4
6
  * #api
5
7
  * The quantile a clip toggle writes: a scale's declared default where that
@@ -37,8 +39,28 @@ export declare function quantileOf(a: Float32Array, n: number, quantile: number)
37
39
  * where the ends meet rather than cross. `scales.y.domainQuantile` and a
38
40
  * colour's `domainQuantile` both name it. `[Infinity, -Infinity]` where
39
41
  * nothing is finite.
42
+ *
43
+ * Nearest rank keeps ties: where one value holds the rank, as on a segmented
44
+ * copy-number track, a ramp clipped below 1 paints nearly every value one
45
+ * colour. A value axis fences instead ({@link fenceOutliers}); a ramp takes
46
+ * the same fence once a colour default below 1 meets tied data.
40
47
  */
41
48
  export declare function quantileExtent(values: ArrayLike<number>, count: number, quantile?: number): [number, number];
49
+ /**
50
+ * #api
51
+ * Where a value axis's open ends stand below a `domainQuantile` of 1: at the
52
+ * `extremes`, unless one would stretch the axis past twice the span the
53
+ * {@link quantileExtent} ends `quantiles` draw, 0 included where the axis
54
+ * reaches it (`zero`); that end stops at the fence. So a plot with no spike
55
+ * shows every value and cuts none, and a spike leaves the values under it at
56
+ * least half the axis. Quantiles that meet span their own size, as a flat
57
+ * domain widens in `getNiceDomain`.
58
+ */
59
+ export declare function fenceOutliers({ extremes, quantiles, zero, }: {
60
+ extremes: readonly [number, number];
61
+ quantiles: readonly [number, number];
62
+ zero: boolean;
63
+ }): [number, number];
42
64
  /**
43
65
  * #api
44
66
  * {@link quantileExtent} as a scale's open ends read it: over every finite
@@ -1,5 +1,7 @@
1
1
  /** The quantile "Clip outliers" clips at on a scale whose own default is the extremes. */
2
2
  export const DEFAULT_CLIP_QUANTILE = 0.99;
3
+ /** The lowest `domainQuantile` a scale reads, where its two ends meet; one below reads as this. */
4
+ export const MIN_DOMAIN_QUANTILE = 0.5;
3
5
  /**
4
6
  * #api
5
7
  * The quantile a clip toggle writes: a scale's declared default where that
@@ -111,12 +113,17 @@ function lowQuantileOf(a, n, quantile) {
111
113
  * where the ends meet rather than cross. `scales.y.domainQuantile` and a
112
114
  * colour's `domainQuantile` both name it. `[Infinity, -Infinity]` where
113
115
  * nothing is finite.
116
+ *
117
+ * Nearest rank keeps ties: where one value holds the rank, as on a segmented
118
+ * copy-number track, a ramp clipped below 1 paints nearly every value one
119
+ * colour. A value axis fences instead ({@link fenceOutliers}); a ramp takes
120
+ * the same fence once a colour default below 1 meets tied data.
114
121
  */
115
122
  export function quantileExtent(values, count, quantile = 1) {
116
123
  if (!(quantile < 1)) {
117
124
  return finiteExtremes(values, count);
118
125
  }
119
- const q = Math.max(0.5, quantile);
126
+ const q = Math.max(MIN_DOMAIN_QUANTILE, quantile);
120
127
  const positive = new Float32Array(count);
121
128
  const negative = new Float32Array(count);
122
129
  let np = 0;
@@ -139,6 +146,22 @@ export function quantileExtent(values, count, quantile = 1) {
139
146
  const high = np > 0 ? quantileOf(positive, np, q) : quantileOf(negative, nn, q);
140
147
  return [Math.min(low, high), high];
141
148
  }
149
+ /**
150
+ * #api
151
+ * Where a value axis's open ends stand below a `domainQuantile` of 1: at the
152
+ * `extremes`, unless one would stretch the axis past twice the span the
153
+ * {@link quantileExtent} ends `quantiles` draw, 0 included where the axis
154
+ * reaches it (`zero`); that end stops at the fence. So a plot with no spike
155
+ * shows every value and cuts none, and a spike leaves the values under it at
156
+ * least half the axis. Quantiles that meet span their own size, as a flat
157
+ * domain widens in `getNiceDomain`.
158
+ */
159
+ export function fenceOutliers({ extremes, quantiles, zero, }) {
160
+ const lo = zero ? Math.min(0, quantiles[0]) : quantiles[0];
161
+ const hi = zero ? Math.max(0, quantiles[1]) : quantiles[1];
162
+ const span = hi - lo || Math.abs(hi) || 1;
163
+ return [Math.max(extremes[0], lo - span), Math.min(extremes[1], hi + span)];
164
+ }
142
165
  /**
143
166
  * #api
144
167
  * {@link quantileExtent} as a scale's open ends read it: over every finite
@@ -10,3 +10,7 @@ export declare function prefersDarkColorScheme(): boolean;
10
10
  * one preference, and a second spelling of the query is a second answer.
11
11
  */
12
12
  export declare function onColorSchemeChange(listener: () => void): () => void;
13
+ /** Whether Dark Reader darkens the page around canvases it leaves light. */
14
+ export declare function darkReaderIsDark(): boolean;
15
+ /** Call `listener` whenever Dark Reader's scheme mark changes, and return the unsubscribe. */
16
+ export declare function onDarkReaderChange(listener: () => void): () => void;
@@ -25,3 +25,24 @@ export function onColorSchemeChange(listener) {
25
25
  media?.removeEventListener('change', listener);
26
26
  };
27
27
  }
28
+ const DARK_READER_ATTRIBUTE = 'data-darkreader-scheme';
29
+ /** Whether Dark Reader darkens the page around canvases it leaves light. */
30
+ export function darkReaderIsDark() {
31
+ return (typeof document !== 'undefined' &&
32
+ document.documentElement.getAttribute(DARK_READER_ATTRIBUTE) === 'dark');
33
+ }
34
+ /** Call `listener` whenever Dark Reader's scheme mark changes, and return the unsubscribe. */
35
+ export function onDarkReaderChange(listener) {
36
+ if (typeof MutationObserver !== 'function' ||
37
+ typeof document === 'undefined') {
38
+ return () => { };
39
+ }
40
+ const observer = new MutationObserver(listener);
41
+ observer.observe(document.documentElement, {
42
+ attributes: true,
43
+ attributeFilter: [DARK_READER_ATTRIBUTE],
44
+ });
45
+ return () => {
46
+ observer.disconnect();
47
+ };
48
+ }
@@ -113,7 +113,7 @@ interface TabixHeaderSource {
113
113
  * An adapter that stops at `getHeader()` therefore cannot tell "this file has
114
114
  * no header" from "this file's header is not commented", and quietly falls back
115
115
  * to an assumed column layout. That has cost real information more than once: a
116
- * PLINK LD file lost its D' column, so `ldMetric: 'dprime'` silently served r²,
116
+ * PLINK LD file lost its D' column, so `color.field: 'dprime'` silently served r²,
117
117
  * and a bedGraph loses the names of its value columns. Nothing errors, because
118
118
  * the assumed layout parses.
119
119
  *
package/esm/util/tabix.js CHANGED
@@ -133,7 +133,7 @@ function mergeTabixLines(...groups) {
133
133
  * An adapter that stops at `getHeader()` therefore cannot tell "this file has
134
134
  * no header" from "this file's header is not commented", and quietly falls back
135
135
  * to an assumed column layout. That has cost real information more than once: a
136
- * PLINK LD file lost its D' column, so `ldMetric: 'dprime'` silently served r²,
136
+ * PLINK LD file lost its D' column, so `color.field: 'dprime'` silently served r²,
137
137
  * and a bedGraph loses the names of its value columns. Nothing errors, because
138
138
  * the assumed layout parses.
139
139
  *
@@ -1,4 +1,4 @@
1
- import type { AnyConfigurationModel } from '../configuration/index.ts';
1
+ import type { AnyConfiguration } from '../configuration/index.ts';
2
2
  /**
3
3
  * Whether a track config names an assembly the session holds as **temporary** —
4
4
  * the read-vs-ref pair a comparative view synthesizes, which goes away when that
@@ -18,4 +18,4 @@ import type { AnyConfigurationModel } from '../configuration/index.ts';
18
18
  * answer is yes. A second copy of the predicate is the thing to avoid — a copied
19
19
  * guard is where the escape clause gets added to only one of them.
20
20
  */
21
- export declare function namesTemporaryAssembly(session: unknown, trackConf: AnyConfigurationModel | Record<string, unknown>): boolean;
21
+ export declare function namesTemporaryAssembly(session: unknown, trackConf: AnyConfiguration): boolean;
@@ -1,4 +1,4 @@
1
- import { readConfObject } from '../configuration/index.js';
1
+ import { getConfAssemblyNamesOrNone } from './tracks.js';
2
2
  /**
3
3
  * Whether a track config names an assembly the session holds as **temporary** —
4
4
  * the read-vs-ref pair a comparative view synthesizes, which goes away when that
@@ -24,6 +24,5 @@ export function namesTemporaryAssembly(session, trackConf) {
24
24
  if (!temporary?.length) {
25
25
  return false;
26
26
  }
27
- const names = readConfObject(trackConf, 'assemblyNames');
28
- return !!names?.some(name => temporary.some(a => a.name === name));
27
+ return getConfAssemblyNamesOrNone(trackConf).some(name => temporary.some(a => a.name === name));
29
28
  }
@@ -53,7 +53,7 @@ export declare function diffTrackConfig(base: Record<string, unknown>, edited: R
53
53
  * longer has is dropped, so no `null` member reaches the config a track
54
54
  * hydrates from.
55
55
  */
56
- export declare function mergeTrackConfig(base: Record<string, unknown>, delta: Record<string, unknown>): Record<string, unknown>;
56
+ export declare function mergeTrackConfig<BASE extends Record<string, unknown>>(base: BASE, delta: Record<string, unknown>): BASE;
57
57
  /**
58
58
  * A single overridden slot: the dotted `path`, its base `from` and edited `to`,
59
59
  * which is `undefined` for a reset.
@@ -4,7 +4,7 @@ export { makeIndex } from './formatGuessers.ts';
4
4
  export { isLooseTrackConfig, type LooseTrackInput, } from '@jbrowse/add-track-core';
5
5
  import { type LooseTrackInput } from '@jbrowse/add-track-core';
6
6
  import type PluginManager from '../PluginManager.ts';
7
- import type { AnyConfigurationModel } from '../configuration/index.ts';
7
+ import type { AnyConfiguration, AnyConfigurationModel, AnyTrackConfig, TrackConfigEntry } from '../configuration/index.ts';
8
8
  import type { BlobLocation, FileHandleLocation, FileLocation, PreFileLocation } from './types/data.ts';
9
9
  import type { AbstractViewContainer } from './types/index.ts';
10
10
  import type { IAnyStateTreeNode, IAnyType, IStateTreeNode, Instance, types } from '@jbrowse/mobx-state-tree';
@@ -68,7 +68,7 @@ export declare function canonicalAssemblyNames(names: string[], assemblyManager:
68
68
  * to compare equal to themselves.
69
69
  */
70
70
  export declare function isSameAssemblyName(a: string | undefined, b: string | undefined, assemblyManager: AssemblyNameResolver): boolean;
71
- export declare function getConfAssemblyNames(conf: AnyConfigurationModel): string[];
71
+ export declare function getConfAssemblyNames(conf: AnyConfiguration): string[];
72
72
  /**
73
73
  * {@link getConfAssemblyNames} for a caller that cannot afford its throw — a
74
74
  * config answering neither way leaves the question unanswered instead.
@@ -80,7 +80,12 @@ export declare function getConfAssemblyNames(conf: AnyConfigurationModel): strin
80
80
  * answers `[]` for a ReferenceSequenceTrack whose assembly this already knows
81
81
  * how to name.
82
82
  */
83
- export declare function getConfAssemblyNamesOrNone(conf: AnyConfigurationModel): string[];
83
+ export declare function getConfAssemblyNamesOrNone(conf: AnyConfiguration): string[];
84
+ /**
85
+ * A track config as a frozen entry: `conf` itself, or a live node's snapshot.
86
+ * Either way a slot at its default is absent, so a read supplies the default.
87
+ */
88
+ export declare function toTrackConfigEntry(conf: AnyTrackConfig): TrackConfigEntry;
84
89
  /**
85
90
  * The RPC session id a track's own requests go out under, from its config:
86
91
  * what `BaseTrackModel.rpcSessionId` computes, for a caller holding no model.
@@ -277,11 +282,7 @@ export declare function generateUnknownTrackConf(trackName: string, trackUrl: st
277
282
  category: string[] | undefined;
278
283
  trackId: string;
279
284
  };
280
- export declare function getTrackName(conf: AnyConfigurationModel | {
281
- name?: string;
282
- type?: string;
283
- trackId?: string;
284
- }, session: {
285
+ export declare function getTrackName(conf: AnyConfiguration, session: {
285
286
  assemblies: AnyConfigurationModel[];
286
287
  }): string;
287
288
  type MSTArray<T extends IAnyType> = Instance<ReturnType<typeof types.array<T>>>;
@@ -347,22 +348,22 @@ export declare function viewCanDisplayTrack(pluginManager: PluginManager, viewDi
347
348
  * and every other picker that opens a track into a view read this one answer,
348
349
  * or one of them offers a track the next hides.
349
350
  */
350
- export declare function filterTracks(tracks: AnyConfigurationModel[], self: {
351
+ export declare function filterTracks<T extends AnyTrackConfig>(tracks: T[], self: {
351
352
  view?: IStateTreeNode & {
352
353
  type: string;
353
354
  };
354
355
  assemblyNames: string[];
355
- }): AnyConfigurationModel[];
356
+ }): T[];
356
357
  /**
357
358
  * Every track config a view can be offered, each assembly's sequence track
358
359
  * first and then {@link filterTracks} over the list handed in.
359
360
  */
360
- export declare function offeredTracks(tracks: AnyConfigurationModel[], self: {
361
+ export declare function offeredTracks(tracks: AnyTrackConfig[], self: {
361
362
  view?: IStateTreeNode & {
362
363
  type: string;
363
364
  };
364
365
  assemblyNames: string[];
365
- }): AnyConfigurationModel[];
366
+ }): AnyTrackConfig[];
366
367
  /**
367
368
  * Which display a track opens with in a given view, and the declared entry
368
369
  * that configures it: `{ type, conf }`, or undefined when the view draws none
@@ -408,6 +409,15 @@ export declare function normalizeTrackInit(t: TrackInit): {
408
409
  [x: string]: unknown;
409
410
  };
410
411
  };
412
+ /**
413
+ * The display type a view of `viewType` opens the track `rawConf` as, the one
414
+ * `showTrack` picks, or undefined when the view draws none of the track's
415
+ * displays. For a caller that needs to know the display before the view exists,
416
+ * such as a CLI choosing which settings to write.
417
+ */
418
+ export declare function trackDisplayType(pluginManager: PluginManager, rawConf: {
419
+ type: string;
420
+ }, viewType: string, requested?: string): string | undefined;
411
421
  export declare function showTrackGeneric(self: GenericView, trackId: string, initialSnapshot?: object, displayInitialSnapshot?: DisplayInitialSnapshot, inlineConf?: Record<string, unknown>): any;
412
422
  export interface DisplaySnapshot {
413
423
  id: string;
@@ -446,11 +456,11 @@ export declare function launchToggleTrackGeneric(self: GenericView, trackId: str
446
456
  * yields every session track twice.
447
457
  */
448
458
  export declare function allSessionTracks(session: {
449
- tracks: AnyConfigurationModel[];
459
+ tracks: TrackConfigEntry[];
450
460
  connectionInstances?: {
451
461
  tracks: AnyConfigurationModel[];
452
462
  }[];
453
- }): AnyConfigurationModel[];
463
+ }): AnyTrackConfig[];
454
464
  /**
455
465
  * The session's annotation for `assemblyName`, as trackIds: what a view opened
456
466
  * on another genome starts with. Only FeatureTracks, not everything configured
@@ -459,7 +469,7 @@ export declare function allSessionTracks(session: {
459
469
  * range query.
460
470
  */
461
471
  export declare function annotationTrackIds(session: {
462
- tracks: AnyConfigurationModel[];
472
+ tracks: TrackConfigEntry[];
463
473
  connectionInstances?: {
464
474
  tracks: AnyConfigurationModel[];
465
475
  }[];
@@ -13,7 +13,7 @@ export { isLooseTrackConfig, } from '@jbrowse/add-track-core';
13
13
  import { isLooseTrackConfig, } from '@jbrowse/add-track-core';
14
14
  import { asModelType, getParent, getSnapshot, getType, hasParent, isAlive, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
15
15
  import { observable, runInAction, untracked } from 'mobx';
16
- import { readConfObject } from '../configuration/index.js';
16
+ import { isConfigurationModel, readConfObject } from '../configuration/index.js';
17
17
  import { adapterConfigCacheKey } from '../data_adapters/dataAdapterCache.js';
18
18
  import { displayCandidates, displayTypesFedBy, } from '../pluggableElementTypes/models/baseTrackConfig.js';
19
19
  import { getFileHandle, storeFileHandle, verifyPermission, } from './fileHandleStore.js';
@@ -90,8 +90,14 @@ export function isSameAssemblyName(a, b, assemblyManager) {
90
90
  * copy a non-admin's edits would go to, ADR-032). `hasParent` is the guard:
91
91
  * without it the walk raises MST's "Failed to find the parent" from a helper
92
92
  * whose other two failure modes are a plain `undefined`.
93
+ *
94
+ * A frozen {@link TrackConfigEntry} omits a slot at its default, so one
95
+ * without `assemblyNames` names the slot's default, `[]`.
93
96
  */
94
97
  function confAssemblyNames(conf) {
98
+ if (!isConfigurationModel(conf)) {
99
+ return conf.assemblyNames ?? [];
100
+ }
95
101
  const trackAssemblyNames = readConfObject(conf, 'assemblyNames');
96
102
  if (trackAssemblyNames) {
97
103
  return trackAssemblyNames;
@@ -125,6 +131,13 @@ export function getConfAssemblyNames(conf) {
125
131
  export function getConfAssemblyNamesOrNone(conf) {
126
132
  return confAssemblyNames(conf) ?? [];
127
133
  }
134
+ /**
135
+ * A track config as a frozen entry: `conf` itself, or a live node's snapshot.
136
+ * Either way a slot at its default is absent, so a read supplies the default.
137
+ */
138
+ export function toTrackConfigEntry(conf) {
139
+ return isStateTreeNode(conf) ? getSnapshot(conf) : conf;
140
+ }
128
141
  /**
129
142
  * The RPC session id a track's own requests go out under, from its config:
130
143
  * what `BaseTrackModel.rpcSessionId` computes, for a caller holding no model.
@@ -463,21 +476,16 @@ export function generateUnknownTrackConf(trackName, trackUrl, categories) {
463
476
  return generateProblemTrackConf(trackName, categories, 'Unknown', `Could not determine track type for "${trackUrl}"`);
464
477
  }
465
478
  export function getTrackName(conf, session) {
466
- const isMst = isStateTreeNode(conf);
467
- const trackName = isMst
479
+ const trackName = isConfigurationModel(conf)
468
480
  ? readConfObject(conf, 'name')
469
481
  : (conf.name ?? '');
470
- const trackType = isMst ? readConfObject(conf, 'type') : conf.type;
471
- if (!trackName && trackType === 'ReferenceSequenceTrack') {
482
+ if (!trackName && conf.type === 'ReferenceSequenceTrack') {
472
483
  const asm = session.assemblies.find(a => a.sequence === conf);
473
484
  return asm
474
485
  ? `Reference sequence (${readConfObject(asm, 'displayName') || readConfObject(asm, 'name')})`
475
486
  : 'Reference sequence';
476
487
  }
477
- const trackId = isMst
478
- ? readConfObject(conf, 'trackId')
479
- : (conf.trackId ?? '');
480
- return trackName || trackId;
488
+ return trackName || (conf.trackId ?? '');
481
489
  }
482
490
  // The first-show path spreads the same settings into the display's `create()`
483
491
  // snapshot, so a DECLARED PROP (`resolution` on the wiggle displays) has
@@ -614,12 +622,11 @@ export function filterTracks(tracks, self) {
614
622
  const viewAssemblyNames = canonical(self.assemblyNames);
615
623
  const viewDisplays = viewDisplayNames(pluginManager, view.type);
616
624
  return tracks.filter(c => {
617
- const trackAssemblyNames = readConfObject(c, 'assemblyNames');
618
625
  return (
619
626
  // a view that declares no assemblies (one still initializing) constrains
620
627
  // nothing; otherwise the track must cover every one of them
621
628
  (viewAssemblyNames.length === 0 ||
622
- containsAll(trackAssemblyNames && canonical(trackAssemblyNames), viewAssemblyNames)) &&
629
+ containsAll(canonical(getConfAssemblyNamesOrNone(c)), viewAssemblyNames)) &&
623
630
  viewCanDisplayTrack(pluginManager, viewDisplays, {
624
631
  type: c.type,
625
632
  adapter: c.adapter,
@@ -705,6 +712,43 @@ export function normalizeTrackInit(t) {
705
712
  };
706
713
  }
707
714
  }
715
+ /**
716
+ * The display a view of `viewType` opens `conf` as, chosen from the displays the
717
+ * track's adapter feeds and the view draws. `requested` is the display type a
718
+ * caller asks for; a name no display answers to leaves `picked` undefined.
719
+ * Aliases resolve first, so a caller or a config naming a pre-consolidation
720
+ * display type (`LinearPileupDisplay`) reaches the type that replaced it.
721
+ */
722
+ function pickTrackDisplay(pluginManager, conf, viewTypeName, requested) {
723
+ const viewType = pluginManager.getViewType(viewTypeName);
724
+ const candidates = displayCandidates(pluginManager, conf);
725
+ const viewDisplayTypes = viewType.displayTypes.map(d => d.name);
726
+ const canonical = (name) => pluginManager.resolveDisplayTypeRecord(name)?.name ?? name;
727
+ const declaredDisplays = conf.displays ?? [];
728
+ const picked = pickDisplayForView({
729
+ candidates,
730
+ declaredDisplays: declaredDisplays.map(d => ({
731
+ ...d,
732
+ type: canonical(d.type),
733
+ })),
734
+ requestedType: requested === undefined ? undefined : canonical(requested),
735
+ viewDisplayTypes,
736
+ preferredDisplayTypes: new Set(viewType.displayTypes
737
+ .filter(d => d.viewType === viewTypeName)
738
+ .map(d => d.name)),
739
+ });
740
+ return { picked, candidates, viewDisplayTypes };
741
+ }
742
+ /**
743
+ * The display type a view of `viewType` opens the track `rawConf` as, the one
744
+ * `showTrack` picks, or undefined when the view draws none of the track's
745
+ * displays. For a caller that needs to know the display before the view exists,
746
+ * such as a CLI choosing which settings to write.
747
+ */
748
+ export function trackDisplayType(pluginManager, rawConf, viewType, requested) {
749
+ const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', structuredClone(rawConf));
750
+ return pickTrackDisplay(pluginManager, conf, viewType, requested).picked?.type;
751
+ }
708
752
  function resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf) {
709
753
  const { pluginManager } = getEnv(self);
710
754
  const session = getSession(self);
@@ -747,27 +791,8 @@ function resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inline
747
791
  // display choice from the view it sits in, which is the same node
748
792
  // getContainingView already resolves to for everything else beneath it.
749
793
  const view = isViewModel(self) ? self : getContainingView(self);
750
- const viewType = pluginManager.getViewType(view.type);
751
- const candidates = displayCandidates(pluginManager, conf);
752
- const viewDisplayTypes = viewType.displayTypes.map(d => d.name);
753
- // Aliases resolve first, so a caller or a config naming a pre-consolidation
754
- // display type (`LinearPileupDisplay`) reaches the type that replaced it; a
755
- // name no display answers to stays as written, for the refusal to quote.
756
- const canonical = (name) => pluginManager.resolveDisplayTypeRecord(name)?.name ?? name;
757
794
  const requested = displayInitialSnapshot.type;
758
- const declaredDisplays = conf.displays ?? [];
759
- const picked = pickDisplayForView({
760
- candidates,
761
- declaredDisplays: declaredDisplays.map(d => ({
762
- ...d,
763
- type: canonical(d.type),
764
- })),
765
- requestedType: requested === undefined ? undefined : canonical(requested),
766
- viewDisplayTypes,
767
- preferredDisplayTypes: new Set(viewType.displayTypes
768
- .filter(d => d.viewType === view.type)
769
- .map(d => d.name)),
770
- });
795
+ const { picked, candidates, viewDisplayTypes } = pickTrackDisplay(pluginManager, conf, view.type, requested);
771
796
  if (!picked) {
772
797
  const drawable = candidates.filter(name => viewDisplayTypes.includes(name));
773
798
  throw new Error(requested === undefined
@@ -898,11 +923,9 @@ export function warmTrackDisplayGeneric(self, trackId, displayInitialSnapshot =
898
923
  const { pluginManager } = getEnv(self);
899
924
  try {
900
925
  const { picked } = resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot);
901
- pluginManager
902
- .resolveDisplayTypeRecord(picked.type)
903
- ?.loadStateModel()
904
- .catch(() => { });
905
- warmTrackAdapter(self, trackId, assemblyName);
926
+ const display = pluginManager.resolveDisplayTypeRecord(picked.type);
927
+ display?.loadStateModel().catch(() => { });
928
+ warmTrackAdapter(self, trackId, display?.rpcMethods ?? [], assemblyName);
906
929
  }
907
930
  catch {
908
931
  // the launch reports an unresolvable track
@@ -921,8 +944,8 @@ function adapterTypesIn(value, pluginManager, found = new Set()) {
921
944
  return found;
922
945
  }
923
946
  /**
924
- * Load the track's adapter code and read its index in the worker, beside the
925
- * assembly load the track's first request waits for.
947
+ * Load the track's adapter code and its display's RPC code, and read its index,
948
+ * in the worker, beside the assembly load the track's first request waits for.
926
949
  *
927
950
  * Sent under the id the track's own requests will use, so it reaches, and
928
951
  * boots, the worker that serves them. That id hashes the adapter config as the
@@ -942,7 +965,7 @@ function declaresLanes(adapterConfig, pluginManager) {
942
965
  .getAdapterType(type)
943
966
  .adapterCapabilities.includes('headerLanes'));
944
967
  }
945
- function warmTrackAdapter(self, trackId, assemblyName) {
968
+ function warmTrackAdapter(self, trackId, rpcMethods, assemblyName) {
946
969
  const { pluginManager } = getEnv(self);
947
970
  const session = getSession(self);
948
971
  const raw = session.getTrackById(trackId);
@@ -958,8 +981,9 @@ function warmTrackAdapter(self, trackId, assemblyName) {
958
981
  const sessionId = getConfRpcSessionId(conf);
959
982
  session.rpcManager
960
983
  // eslint-disable-next-line no-restricted-syntax -- a code download the track's first request would make anyway: nothing to show, and nothing a user can move on from
961
- .call(sessionId, 'CoreLoadAdapterCode', {
984
+ .call(sessionId, 'CoreLoadTrackCode', {
962
985
  adapterTypes: [...adapterTypesIn(adapterConfig, pluginManager)],
986
+ rpcMethods: [...rpcMethods],
963
987
  })
964
988
  .catch(() => { });
965
989
  // a source that declares its lanes reads an index per lane asked about,
@@ -1046,9 +1070,9 @@ export function allSessionTracks(session) {
1046
1070
  * range query.
1047
1071
  */
1048
1072
  export function annotationTrackIds(session, assemblyName) {
1049
- return allSessionTracks(session).flatMap(track => readConfObject(track, 'type') === 'FeatureTrack' &&
1050
- readConfObject(track, 'assemblyNames').some(name => isSameAssemblyName(name, assemblyName, session.assemblyManager))
1051
- ? [readConfObject(track, 'trackId')]
1073
+ return allSessionTracks(session).flatMap(track => track.type === 'FeatureTrack' &&
1074
+ getConfAssemblyNamesOrNone(track).some(name => isSameAssemblyName(name, assemblyName, session.assemblyManager))
1075
+ ? [track.trackId]
1052
1076
  : []);
1053
1077
  }
1054
1078
  function isNavigableView(view) {