@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
@@ -4,6 +4,13 @@ import { createStatusFanOut } from '../../util/progress.js';
4
4
  import { blankStats, scoresToStats } from '../../util/stats.js';
5
5
  import { BaseAdapter } from './BaseAdapter.js';
6
6
  import { aggregateQuantitativeStats } from './stats.js';
7
+ import { isFeatureAdapter } from './util.js';
8
+ function isAdapterConfigSnapshot(value) {
9
+ return (typeof value === 'object' &&
10
+ value !== null &&
11
+ 'type' in value &&
12
+ typeof value.type === 'string');
13
+ }
7
14
  /**
8
15
  * Base class for feature adapters to extend. Defines some methods that
9
16
  * subclasses must implement.
@@ -110,6 +117,38 @@ export class BaseFeatureDataAdapter extends BaseAdapter {
110
117
  async getRegionByteSize(_regions, _opts) {
111
118
  return undefined;
112
119
  }
120
+ /**
121
+ * Features per bin over each region, at the view's `bpPerPx`, for the density
122
+ * tier the display draws where the region-too-large gate refused the
123
+ * features. The default reads the `densityAdapter` sidecar slot
124
+ * (`densityAdapterConfigSchemaFields`) through `getSubAdapter`; `undefined`
125
+ * means no tier, and the banner stands. ADR-102 records why no index
126
+ * estimate stands in for a missing sidecar.
127
+ */
128
+ async getFeatureDensity(regions, opts) {
129
+ const sidecar = this.getConf(['densityAdapter']);
130
+ const resolved = isAdapterConfigSnapshot(sidecar) && this.getSubAdapter
131
+ ? (await this.getSubAdapter(sidecar)).dataAdapter
132
+ : undefined;
133
+ if (resolved && !isFeatureAdapter(resolved)) {
134
+ throw new Error(`densityAdapter ${isAdapterConfigSnapshot(sidecar) ? sidecar.type : ''} is not a feature adapter`);
135
+ }
136
+ return resolved
137
+ ? Promise.all(regions.map(async (region) => {
138
+ const features = await resolved.getFeaturesArray(region, opts);
139
+ const starts = new Uint32Array(features.length);
140
+ const ends = new Uint32Array(features.length);
141
+ const scores = new Float32Array(features.length);
142
+ features.forEach((feature, i) => {
143
+ const score = feature.get('score');
144
+ starts[i] = feature.get('start');
145
+ ends[i] = feature.get('end');
146
+ scores[i] = score === undefined ? 0 : score;
147
+ });
148
+ return { starts, ends, scores };
149
+ }))
150
+ : undefined;
151
+ }
113
152
  async getSources(regions, opts = {}) {
114
153
  const features = await this.getFeaturesInMultipleRegionsArray(regions, opts);
115
154
  const sources = new Set();
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A density source's intervals over one region, the payload of the density
3
+ * tier that stands in for the features a gated fetch refused: features per bin
4
+ * for a `make-density` sidecar, whatever the bigWig measures otherwise, each
5
+ * value a level over its span so a zoomed read means the same thing. Absolute
6
+ * genomic coordinates.
7
+ */
8
+ export interface FeatureDensity {
9
+ starts: Uint32Array;
10
+ ends: Uint32Array;
11
+ scores: Float32Array;
12
+ }
13
+ /**
14
+ * The slot a feature adapter spreads to take a density sidecar — a
15
+ * quantitative sub-adapter (typically a BigWig of feature starts per bin) the
16
+ * display reads at the view's bp/px in place of features the region-too-large
17
+ * gate refused. Mirrors MAF's `summaryAdapter`: on the adapter, so the tier
18
+ * names the file it summarizes.
19
+ */
20
+ export declare const densityAdapterConfigSchemaFields: {
21
+ /**
22
+ * #slot
23
+ */
24
+ readonly densityAdapter: {
25
+ readonly type: 'frozen';
26
+ readonly defaultValue: null;
27
+ readonly description: 'optional quantitative sub-adapter (e.g. a BigWigAdapter over a features-per-bin bigWig) drawn as a density band where the region is too large to fetch features; null disables it';
28
+ };
29
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The slot a feature adapter spreads to take a density sidecar — a
3
+ * quantitative sub-adapter (typically a BigWig of feature starts per bin) the
4
+ * display reads at the view's bp/px in place of features the region-too-large
5
+ * gate refused. Mirrors MAF's `summaryAdapter`: on the adapter, so the tier
6
+ * names the file it summarizes.
7
+ */
8
+ export const densityAdapterConfigSchemaFields = {
9
+ /**
10
+ * #slot
11
+ */
12
+ densityAdapter: {
13
+ type: 'frozen',
14
+ defaultValue: null,
15
+ description: 'optional quantitative sub-adapter (e.g. a BigWigAdapter over a features-per-bin bigWig) drawn as a density band where the region is too large to fetch features; null disables it',
16
+ },
17
+ };
@@ -7,6 +7,7 @@ export type { BaseOptions, BaseOptionsWithRegions, BaseTextSearchArgs, SearchTyp
7
7
  export { BaseAdapter } from './BaseAdapter.ts';
8
8
  export { cachedSetup } from '../../util/cachedSetup.ts';
9
9
  export { BaseFeatureDataAdapter } from './BaseFeatureDataAdapter.ts';
10
+ export { type FeatureDensity, densityAdapterConfigSchemaFields, } from './featureDensity.ts';
10
11
  export { BaseSequenceAdapter } from './BaseSequenceAdapter.ts';
11
12
  export type { BaseTextSearchAdapter } from './BaseTextSearchAdapter.ts';
12
13
  export type { Alias, BaseRefNameAliasAdapter, } from './BaseRefNameAliasAdapter.ts';
@@ -2,4 +2,5 @@ export { isFeatureAdapter, isRefNameAliasAdapter, isRegionsAdapter, isSequenceAd
2
2
  export { BaseAdapter } from './BaseAdapter.js';
3
3
  export { cachedSetup } from '../../util/cachedSetup.js';
4
4
  export { BaseFeatureDataAdapter } from './BaseFeatureDataAdapter.js';
5
+ export { densityAdapterConfigSchemaFields, } from './featureDensity.js';
5
6
  export { BaseSequenceAdapter } from './BaseSequenceAdapter.js';
@@ -3,8 +3,40 @@ import type { RectifiedQuantitativeStats } from '../../util/stats.ts';
3
3
  import type { AugmentedRegion as Region } from '../../util/types/index.ts';
4
4
  import type { BaseOptions } from './types.ts';
5
5
  import type { Observable } from 'rxjs';
6
+ /**
7
+ * What a caller that will *judge* the density knows and the probe does not: how
8
+ * wide a window has to be before its count can settle that judgement, and when
9
+ * a count has settled it. Supplying it turns the ladder below from "grow until
10
+ * the estimate is precise" into "grow until the verdict is decided", which at
11
+ * whole-genome zoom is one window instead of thirteen.
12
+ *
13
+ * The probe stays ignorant of what the budget means — `plugins/canvas`'s
14
+ * `densityProbeGate` owns both numbers, so the comparison that refuses a region
15
+ * lives beside the one the banner re-derives rather than in a second copy here.
16
+ */
17
+ export interface DensityProbeGate {
18
+ /** First window to sample: the narrowest one whose count can settle it. */
19
+ initialInterval: number;
20
+ /** Whether this many admitted features in this many bp decides the verdict. */
21
+ settled: (admitted: number, sampledBp: number) => boolean;
22
+ }
23
+ /**
24
+ * The adapter options a probe forwards, plus the two things only its caller
25
+ * knows. Named rather than trailing positionals: `BaseOptions` has no required
26
+ * member, so every one of its slots structurally accepts anything, and an
27
+ * `admit` handed to the `opts` slot typechecks and then never filters.
28
+ */
29
+ export interface DensityProbeOptions extends BaseOptions {
30
+ /**
31
+ * The caller's feature-admission predicate (config filters, type gates, ...).
32
+ * See the note on the growth/report asymmetry below.
33
+ */
34
+ admit?: (feature: Feature) => boolean;
35
+ /** Stop as soon as the verdict is decided rather than when it is precise. */
36
+ gate?: DensityProbeGate;
37
+ }
6
38
  export declare function aggregateQuantitativeStats(stats: RectifiedQuantitativeStats[]): RectifiedQuantitativeStats;
7
- export declare function calculateFeatureDensityStats(region: Region, getFeatures: (region: Region, opts?: BaseOptions) => Observable<Feature>, opts?: BaseOptions, admit?: (feature: Feature) => boolean): Promise<{
39
+ export declare function calculateFeatureDensityStats(region: Region, getFeatures: (region: Region, opts?: BaseOptions) => Observable<Feature>, { admit, gate, ...opts }?: DensityProbeOptions): Promise<{
8
40
  featureDensity: number;
9
41
  }>;
10
42
  export { blankStats } from '../../util/stats.ts';
@@ -19,13 +19,16 @@ export function aggregateQuantitativeStats(stats) {
19
19
  ...(meanMaxs.length > 0 ? { scoreMeanMax: max(meanMaxs) } : {}),
20
20
  });
21
21
  }
22
- // A sample window of `interval` bp centered ~25% into the region, so the sample
23
- // avoids the region edges where coverage often tapers. Clamped to the region
24
- // bounds so it never samples features outside the region being measured (the
25
- // left edge clamps to the region start, not to the chromosome origin).
26
- function sampleWindow(region, interval) {
22
+ // Where the probe samples, as a fraction into the region. The primary point
23
+ // avoids the region edges, where coverage often tapers.
24
+ const DENSITY_SAMPLE_POINT = 0.25;
25
+ const DENSITY_CONFIRM_POINT = 0.7;
26
+ // A sample window of `interval` bp centered `at` into the region. Clamped to the
27
+ // region bounds so it never samples features outside the region being measured
28
+ // (the left edge clamps to the region start, not to the chromosome origin).
29
+ function sampleWindow(region, interval, at) {
27
30
  const { start, end } = region;
28
- const sampleCenter = start * 0.75 + end * 0.25;
31
+ const sampleCenter = start + (end - start) * at;
29
32
  return {
30
33
  start: Math.max(start, Math.round(sampleCenter - interval / 2)),
31
34
  end: Math.min(end, Math.round(sampleCenter + interval / 2)),
@@ -49,20 +52,56 @@ function sampleWindow(region, interval) {
49
52
  // count would make a selective filter double the window over and over until it
50
53
  // spanned the whole region — turning the cheap pre-fetch probe into a second
51
54
  // full download for exactly the views that need it most.
52
- export async function calculateFeatureDensityStats(region, getFeatures, opts, admit) {
55
+ export async function calculateFeatureDensityStats(region, getFeatures, { admit, gate, ...opts } = {}) {
53
56
  const refLen = region.end - region.start;
54
57
  const t0 = performance.now();
55
- let interval = DENSITY_SAMPLE_INITIAL_INTERVAL;
58
+ let interval = Math.max(DENSITY_SAMPLE_INITIAL_INTERVAL, gate?.initialInterval ?? 0);
59
+ const sampleAt = async (at) => {
60
+ const { start, end } = sampleWindow(region, interval, at);
61
+ const features = await firstValueFrom(getFeatures({ ...region, start, end }, opts).pipe(toArray()));
62
+ return {
63
+ sampledBp: end - start,
64
+ raw: features.length,
65
+ admitted: admit ? features.filter(admit).length : features.length,
66
+ };
67
+ };
56
68
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
57
69
  while (true) {
58
- const { start, end } = sampleWindow(region, interval);
59
- const sampledBp = end - start;
60
- const features = await firstValueFrom(getFeatures({ ...region, start, end }, opts).pipe(toArray()));
61
- const admitted = admit ? features.filter(admit).length : features.length;
70
+ const { sampledBp, raw, admitted } = await sampleAt(DENSITY_SAMPLE_POINT);
71
+ // The verdict is already decided by a margin, so a precise density would
72
+ // only make it more decided. Tested on the admitted count, which is the
73
+ // population the caller judges — the raw count below governs window growth
74
+ // for the reason in the note above, and the two must not be swapped.
75
+ //
76
+ // A second point has to agree before the probe answers a verdict the old
77
+ // ladder could not have reached — and `raw` is exactly what separates the
78
+ // two. A window already holding DENSITY_SAMPLE_MIN_FEATURES is one the exit
79
+ // below would have answered from anyway, so there is nothing new to confirm
80
+ // and this falls through to it for the same number. Under that count the
81
+ // ladder would have kept widening, diluting a local cluster, and answering
82
+ // here instead is what lets a sparse track with a cluster at the fixed
83
+ // sample point read as dense and banner at that zoom until the user
84
+ // force-loads.
85
+ //
86
+ // So the confirmation is owed only by the case that is new, which is also
87
+ // the case that is cheap: every dense annotation track measured clears
88
+ // DENSITY_SAMPLE_MIN_FEATURES in its first window and pays nothing. A
89
+ // disagreement is not a verdict — the ladder simply carries on.
90
+ if (gate?.settled(admitted, sampledBp) &&
91
+ raw < DENSITY_SAMPLE_MIN_FEATURES) {
92
+ const other = await sampleAt(DENSITY_CONFIRM_POINT);
93
+ if (gate.settled(other.admitted, other.sampledBp)) {
94
+ // the lower of two agreeing readings, so the count the banner quotes is
95
+ // never the high one of a pair
96
+ return {
97
+ featureDensity: Math.min(admitted / sampledBp, other.admitted / other.sampledBp),
98
+ };
99
+ }
100
+ }
62
101
  // Enough features to be meaningful, or the window already spans the whole
63
102
  // region (growing further can't sample more) — report density over the bp
64
103
  // actually sampled, not the nominal interval.
65
- if (features.length >= DENSITY_SAMPLE_MIN_FEATURES || sampledBp >= refLen) {
104
+ if (raw >= DENSITY_SAMPLE_MIN_FEATURES || sampledBp >= refLen) {
66
105
  return { featureDensity: admitted / sampledBp };
67
106
  }
68
107
  // Sparse region or slow adapter: sampling ran past the time budget without
@@ -13,6 +13,7 @@ export interface BaseOptions {
13
13
  assemblyName?: string;
14
14
  targetAssemblyName?: string;
15
15
  lodMode?: 'fine' | 'coarse';
16
+ topLevelOnly?: boolean;
16
17
  }
17
18
  export interface BaseOptionsWithRegions extends BaseOptions {
18
19
  regions?: Region[];
@@ -3,17 +3,17 @@ import type { AnyConfigurationSchemaType } from '../configuration/index.ts';
3
3
  import type { AnyReactComponentType } from '../util/index.ts';
4
4
  import type { IAnyModelType } from '@jbrowse/mobx-state-tree';
5
5
  export default class DisplayType extends PluggableElementBase {
6
- stateModel: IAnyModelType;
6
+ private loadedStateModel?;
7
+ get stateModel(): IAnyModelType;
8
+ stateModelLoader?: () => Promise<IAnyModelType>;
9
+ private stateModelPromise?;
10
+ private pendingStateModelExtensions;
7
11
  configSchema: AnyConfigurationSchemaType;
8
12
  ReactComponent: AnyReactComponentType;
9
13
  /**
10
14
  * The track type the display is associated with
11
15
  */
12
16
  trackType: string;
13
- subDisplay?: {
14
- type: string;
15
- [key: string]: unknown;
16
- };
17
17
  /**
18
18
  * The view type the display is associated with
19
19
  */
@@ -24,17 +24,16 @@ export default class DisplayType extends PluggableElementBase {
24
24
  helpText?: string;
25
25
  constructor(stuff: {
26
26
  name: string;
27
- stateModel: IAnyModelType;
27
+ stateModel: IAnyModelType | (() => Promise<IAnyModelType>);
28
28
  trackType: string;
29
29
  viewType: string;
30
30
  displayName?: string;
31
- subDisplay?: {
32
- type: string;
33
- [key: string]: unknown;
34
- };
35
31
  configSchema: AnyConfigurationSchemaType;
36
32
  ReactComponent: AnyReactComponentType;
37
33
  helpText?: string;
38
34
  aliases?: string[];
39
35
  });
36
+ get isStateModelLoaded(): boolean;
37
+ loadStateModel(): Promise<IAnyModelType>;
38
+ extendStateModel(extend: (stateModel: IAnyModelType) => IAnyModelType): void;
40
39
  }
@@ -1,17 +1,19 @@
1
1
  import PluggableElementBase from './PluggableElementBase.js';
2
2
  export default class DisplayType extends PluggableElementBase {
3
- stateModel;
3
+ loadedStateModel;
4
+ // same contract as ViewType.stateModel
5
+ get stateModel() {
6
+ return this.loadedStateModel;
7
+ }
8
+ stateModelLoader;
9
+ stateModelPromise;
10
+ pendingStateModelExtensions = [];
4
11
  configSchema;
5
12
  ReactComponent;
6
13
  /**
7
14
  * The track type the display is associated with
8
15
  */
9
16
  trackType;
10
- /*
11
- * Indicates that this display type can be a "sub-display" of another type of
12
- * display, e.g. in AlignmentsDisplay, has Pileup and SNPCoverage subDisplays
13
- */
14
- subDisplay;
15
17
  /**
16
18
  * The view type the display is associated with
17
19
  */
@@ -22,12 +24,45 @@ export default class DisplayType extends PluggableElementBase {
22
24
  helpText;
23
25
  constructor(stuff) {
24
26
  super(stuff);
25
- this.stateModel = stuff.stateModel;
26
- this.subDisplay = stuff.subDisplay;
27
+ if (typeof stuff.stateModel === 'function') {
28
+ this.stateModelLoader = stuff.stateModel;
29
+ }
30
+ else {
31
+ this.loadedStateModel = stuff.stateModel;
32
+ }
27
33
  this.configSchema = stuff.configSchema;
28
34
  this.ReactComponent = stuff.ReactComponent;
29
35
  this.trackType = stuff.trackType;
30
36
  this.viewType = stuff.viewType;
31
37
  this.helpText = stuff.helpText;
32
38
  }
39
+ get isStateModelLoaded() {
40
+ return this.loadedStateModel !== undefined;
41
+ }
42
+ loadStateModel() {
43
+ if (this.loadedStateModel !== undefined) {
44
+ return Promise.resolve(this.loadedStateModel);
45
+ }
46
+ if (!this.stateModelLoader) {
47
+ throw new Error(`display type ${this.name} has neither a state model nor a loader`);
48
+ }
49
+ this.stateModelPromise ??= this.stateModelLoader().then(loaded => {
50
+ let stateModel = loaded;
51
+ for (const extend of this.pendingStateModelExtensions) {
52
+ stateModel = extend(stateModel);
53
+ }
54
+ this.pendingStateModelExtensions = [];
55
+ this.loadedStateModel = stateModel;
56
+ return stateModel;
57
+ });
58
+ return this.stateModelPromise;
59
+ }
60
+ extendStateModel(extend) {
61
+ if (this.loadedStateModel !== undefined) {
62
+ this.loadedStateModel = extend(this.loadedStateModel);
63
+ }
64
+ else {
65
+ this.pendingStateModelExtensions.push(extend);
66
+ }
67
+ }
33
68
  }
@@ -1,4 +1,5 @@
1
1
  import PluggableElementBase from './PluggableElementBase.ts';
2
+ import type { LaunchKeyRegistration } from '../util/withLaunchInput.ts';
2
3
  import type DisplayType from './DisplayType.ts';
3
4
  import type { IAnyModelType, IAnyStateTreeNode } from '@jbrowse/mobx-state-tree';
4
5
  import type React from 'react';
@@ -12,19 +13,41 @@ interface ViewMetadata {
12
13
  }
13
14
  export default class ViewType extends PluggableElementBase {
14
15
  ReactComponent: ViewComponentType;
15
- stateModel: IAnyModelType;
16
+ private loadedStateModel?;
17
+ get stateModel(): IAnyModelType;
18
+ stateModelLoader?: () => Promise<IAnyModelType>;
19
+ private stateModelPromise?;
20
+ private pendingStateModelExtensions;
21
+ launchKeys?: LaunchKeyRegistration<unknown, string>;
16
22
  displayTypes: DisplayType[];
17
23
  viewMetadata: ViewMetadata;
18
24
  extendedName?: string;
19
25
  constructor(stuff: {
20
26
  name: string;
21
27
  displayName?: string;
22
- stateModel: IAnyModelType;
28
+ stateModel: IAnyModelType | (() => Promise<IAnyModelType>);
29
+ launchKeys?: LaunchKeyRegistration<unknown, string>;
23
30
  extendedName?: string;
24
31
  viewMetadata?: ViewMetadata;
25
32
  ReactComponent: ViewComponentType;
26
33
  aliases?: string[];
27
34
  });
35
+ /**
36
+ * Every key this view accepts written on the view object — its declared MST
37
+ * properties plus the launch keys above. `undefined` for a view that
38
+ * registers none: nothing there says which of its launcher's arguments are
39
+ * settings, so a caller must classify nothing rather than call them all typos.
40
+ * Also `undefined` while a lazily registered state model is not loaded yet,
41
+ * for the same reason: nothing is known, so nothing is a typo.
42
+ *
43
+ * This is the set `withLaunchInput`'s partition reads off the same two
44
+ * declarations, published for the surfaces that never build a view snapshot —
45
+ * a session spec launches straight through `LaunchView-<type>`.
46
+ */
47
+ get acceptedKeys(): string[] | undefined;
48
+ get isStateModelLoaded(): boolean;
49
+ loadStateModel(): Promise<IAnyModelType>;
50
+ extendStateModel(extend: (stateModel: IAnyModelType) => IAnyModelType): void;
28
51
  addDisplayType(display: DisplayType): void;
29
52
  }
30
53
  export {};
@@ -1,7 +1,23 @@
1
1
  import PluggableElementBase from './PluggableElementBase.js';
2
2
  export default class ViewType extends PluggableElementBase {
3
3
  ReactComponent;
4
- stateModel;
4
+ loadedStateModel;
5
+ // Non-optional because nearly every reader holds a view instance, which
6
+ // implies the model is loaded; code that can run before any instance exists
7
+ // (session preloading, pruning, union membership) must check
8
+ // isStateModelLoaded first.
9
+ get stateModel() {
10
+ return this.loadedStateModel;
11
+ }
12
+ // named `stateModel` + `Loader` because pluggableMstType and
13
+ // pruneUnbuildableNodes probe `${fieldName}Loader` generically
14
+ stateModelLoader;
15
+ stateModelPromise;
16
+ pendingStateModelExtensions = [];
17
+ // What `withLaunchInput` partitioned out of this view's snapshots, published
18
+ // beside the model so an out-of-tree plugin, the doc generator and the
19
+ // validator read one declaration rather than each keeping a list.
20
+ launchKeys;
5
21
  displayTypes = [];
6
22
  viewMetadata = {};
7
23
  // extendedName can be used for when you extend a given view type, and want
@@ -16,9 +32,66 @@ export default class ViewType extends PluggableElementBase {
16
32
  super(stuff);
17
33
  this.ReactComponent = stuff.ReactComponent;
18
34
  this.viewMetadata = stuff.viewMetadata ?? {};
19
- this.stateModel = stuff.stateModel;
35
+ if (typeof stuff.stateModel === 'function') {
36
+ this.stateModelLoader = stuff.stateModel;
37
+ }
38
+ else {
39
+ this.loadedStateModel = stuff.stateModel;
40
+ }
41
+ this.launchKeys = stuff.launchKeys;
20
42
  this.extendedName = stuff.extendedName;
21
43
  }
44
+ /**
45
+ * Every key this view accepts written on the view object — its declared MST
46
+ * properties plus the launch keys above. `undefined` for a view that
47
+ * registers none: nothing there says which of its launcher's arguments are
48
+ * settings, so a caller must classify nothing rather than call them all typos.
49
+ * Also `undefined` while a lazily registered state model is not loaded yet,
50
+ * for the same reason: nothing is known, so nothing is a typo.
51
+ *
52
+ * This is the set `withLaunchInput`'s partition reads off the same two
53
+ * declarations, published for the surfaces that never build a view snapshot —
54
+ * a session spec launches straight through `LaunchView-<type>`.
55
+ */
56
+ get acceptedKeys() {
57
+ const properties = this.loadedStateModel?.properties;
58
+ return this.launchKeys && properties
59
+ ? [
60
+ ...Object.keys(properties),
61
+ ...Object.keys(this.launchKeys.keys),
62
+ ...this.launchKeys.passThrough,
63
+ ]
64
+ : undefined;
65
+ }
66
+ get isStateModelLoaded() {
67
+ return this.loadedStateModel !== undefined;
68
+ }
69
+ loadStateModel() {
70
+ if (this.loadedStateModel !== undefined) {
71
+ return Promise.resolve(this.loadedStateModel);
72
+ }
73
+ if (!this.stateModelLoader) {
74
+ throw new Error(`view type ${this.name} has neither a state model nor a loader`);
75
+ }
76
+ this.stateModelPromise ??= this.stateModelLoader().then(loaded => {
77
+ let stateModel = loaded;
78
+ for (const extend of this.pendingStateModelExtensions) {
79
+ stateModel = extend(stateModel);
80
+ }
81
+ this.pendingStateModelExtensions = [];
82
+ this.loadedStateModel = stateModel;
83
+ return stateModel;
84
+ });
85
+ return this.stateModelPromise;
86
+ }
87
+ extendStateModel(extend) {
88
+ if (this.loadedStateModel !== undefined) {
89
+ this.loadedStateModel = extend(this.loadedStateModel);
90
+ }
91
+ else {
92
+ this.pendingStateModelExtensions.push(extend);
93
+ }
94
+ }
22
95
  addDisplayType(display) {
23
96
  this.displayTypes.push(display);
24
97
  }
@@ -45,8 +45,12 @@ export function extendViewType(pluginManager, name, extend) {
45
45
  // point of the signature; widening it once here costs nothing, since
46
46
  // the only operation is handing the model to `extend` and storing what
47
47
  // comes back.
48
+ //
49
+ // Routed through extendStateModel rather than assigning `.stateModel`
50
+ // directly so a view registered with a lazy loader queues the
51
+ // extension and composes it when the loader resolves.
48
52
  const view = element;
49
- view.stateModel = widen(extend)(view.stateModel);
53
+ view.extendStateModel(widen(extend));
50
54
  }
51
55
  return element;
52
56
  });
@@ -59,9 +63,10 @@ export function extendDisplayType(pluginManager, name, extend) {
59
63
  const names = namesOf(name);
60
64
  pluginManager.addToExtensionPoint('Core-extendPluggableElement', (element, props) => {
61
65
  if (props.group === 'display' && names.has(element.name)) {
62
- // widened for the reason in extendViewType
66
+ // widened for the reason in extendViewType; routed through
67
+ // extendStateModel so a lazy registration queues the extension
63
68
  const display = element;
64
- display.stateModel = widen(extend)(display.stateModel);
69
+ display.extendStateModel(widen(extend));
65
70
  }
66
71
  return element;
67
72
  });
@@ -684,6 +684,47 @@ export declare const BaseDisplay: import("@jbrowse/mobx-state-tree").IModelType<
684
684
  * base display reload does nothing, see specialized displays for details
685
685
  */
686
686
  reload(): void;
687
+ /**
688
+ * #action
689
+ * Apply a bag of display settings to the LIVE display, and report what
690
+ * landed. Each key runs through the display config schema's
691
+ * `preProcessSnapshot` (shorthand expansions, legacy-key migrations —
692
+ * the same lowering a session spec's inline track keys get in
693
+ * `showTrackGeneric`), then writes the matching config slot. Keys that
694
+ * are not slots come back in `unapplied` rather than vanishing: the
695
+ * settings vocabulary's historical failure mode is the silently dropped
696
+ * key.
697
+ *
698
+ * `allowSetters` additionally routes a non-slot key to a conventionally
699
+ * named single-argument `set<Key>` action. Opt-in, never the default:
700
+ * the declarative surfaces (session specs, share links, embeds) feed
701
+ * this whole bags of untyped JSON, and a blanket fallback would let
702
+ * them reach internal setters (`setError`, `setScrollTop`, ...) and
703
+ * call multi-argument setters with one argument. A caller that wants a
704
+ * specific action can also simply call it.
705
+ *
706
+ * A key whose write THREW is reported separately, in `failed` — it is the
707
+ * only one of the three that means the caller got something wrong, and
708
+ * the only one worth a notification. `unapplied` is not: at the
709
+ * `showTrackGeneric` call site it also collects keys that surface
710
+ * consumed itself (`type`) and MST display props the display snapshot
711
+ * already applied (`resolution`), so treating it as "dropped" would
712
+ * report a correct call as broken.
713
+ *
714
+ * Per-key errors do not abort the rest of the bag — a caller
715
+ * mid-`showTrack` has already pushed the track, and one rejected value
716
+ * must not strand a half-configured track.
717
+ */
718
+ applyDisplaySettings(settings: Record<string, unknown>, options?: {
719
+ allowSetters?: boolean;
720
+ }): {
721
+ applied: string[];
722
+ unapplied: string[];
723
+ failed: {
724
+ key: string;
725
+ error: string;
726
+ }[];
727
+ };
687
728
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
688
729
  export type BaseDisplayStateModel = typeof BaseDisplay;
689
730
  export type BaseDisplayModel = Instance<BaseDisplayStateModel>;