@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
package/README.md CHANGED
@@ -402,8 +402,11 @@ not be the thing that discovers this. Callers fall back to treating it as the
402
402
  plain object it is.
403
403
 
404
404
  Shares `TrackConfigurationReference`'s per-PluginManager cache, so hydrating the
405
- same entry twice returns the same node and a track that gets opened later reuses
406
- it.
405
+ same entry twice returns the same node — and in admin/embedded sessions a track
406
+ opened later resolves to that same node. A non-admin's open track does not: it
407
+ resolves to the session's private working copy (ADR-032) and this is the
408
+ pristine mirror beside it. The two agree in content, which is what the caller
409
+ needs; `CopyConfigEntryPoints.test.ts` pins both halves.
407
410
 
408
411
  ```js
409
412
  // type signature
@@ -432,6 +435,18 @@ the check off (`HostChecksSlotNames`).
432
435
 
433
436
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/configuration/promotableDefaults.ts)
434
437
 
438
+ ### launchOrReplaceView
439
+
440
+ `addOrReplaceView` for view types whose state model may be lazily loaded; the
441
+ synchronous `addOrReplaceView` requires it loaded already.
442
+
443
+ ```js
444
+ // type signature
445
+ (args: { session: AbstractViewContainer; typeName: string; initialState?: Record<string, unknown> | undefined; replacing?: AbstractViewModel | undefined; }) => Promise<...>
446
+ ```
447
+
448
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/mstUtils.ts)
449
+
435
450
  ### makePin
436
451
 
437
452
  The pin for one promotable slot: "apply this value to every open track of this
@@ -648,6 +663,24 @@ at the point where the connection is cut.
648
663
 
649
664
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/configuration/promotableDefaults.ts)
650
665
 
666
+ ### runLazyAfterAttach
667
+
668
+ Run a display's `afterAttach` body from a module loaded on demand, so the
669
+ autorun installers stay out of the display's eager bundle.
670
+
671
+ One policy for the gap the `await` opens: a node torn down before the module
672
+ lands installs nothing, and a module that fails to load is reported where the
673
+ user can see it rather than onto the display's own error slot — that slot is
674
+ what `reload()` clears, and nothing would re-run the install behind it. Three
675
+ displays hand-rolled this IIFE and each drew the lines differently.
676
+
677
+ ```js
678
+ // type signature
679
+ <Self extends IStateTreeNode>(self: Self, load: () => Promise<(self: Self) => void>) => void
680
+ ```
681
+
682
+ [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/lazyAfterAttach.ts)
683
+
651
684
  ### sampleColorRamp
652
685
 
653
686
  The color at `t` in `[0, 1]` across a list of EVENLY SPACED stops, linearly
@@ -671,6 +704,12 @@ Make JBrowse follow the host's light/dark state — the whole of it, in one moun
671
704
  </SessionPaletteProvider>
672
705
  ```
673
706
 
707
+ `mode` is optional. Left out, JBrowse follows `prefers-color-scheme` and
708
+ re-themes when the OS preference changes, through the same session write an
709
+ explicit mode takes — so a host whose dark mode _is_ the OS preference mounts
710
+ this with a session and nothing else. Pass a mode as soon as the host has a
711
+ toggle of its own, since the media query cannot see it.
712
+
674
713
  A component rather than a documented pair of calls because the pair has a half
675
714
  that can be left out with nothing to show for it. `PaletteProvider` is the name
676
715
  a host reaches for, and it colors the React side alone; the session write is
@@ -684,7 +723,7 @@ colors of its own mounts `PaletteProvider` directly instead.
684
723
 
685
724
  ```js
686
725
  // type signature
687
- ({ session, mode, children, }: { session: ThemeModeSession; mode: "dark" | "light"; children: ReactNode; }) => Element
726
+ ({ session, mode, children, }: { session: ThemeModeSession; mode?: "dark" | "light" | undefined; children: ReactNode; }) => Element
688
727
  ```
689
728
 
690
729
  [Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/ui/PaletteContext.tsx)
@@ -48,9 +48,9 @@ declare const _default: {
48
48
  uniqueId: string;
49
49
  parentId: string;
50
50
  id?: undefined;
51
+ name?: undefined;
51
52
  note?: undefined;
52
53
  protein_id?: undefined;
53
- name?: undefined;
54
54
  } | {
55
55
  transcript_id?: undefined;
56
56
  source: string;
@@ -94,6 +94,7 @@ declare const _default: {
94
94
  transcript_id: string;
95
95
  subfeatures: ({
96
96
  id?: undefined;
97
+ name?: undefined;
97
98
  note?: undefined;
98
99
  protein_id?: undefined;
99
100
  source: string;
@@ -111,7 +112,6 @@ declare const _default: {
111
112
  transcript_id: string;
112
113
  uniqueId: string;
113
114
  parentId: string;
114
- name?: undefined;
115
115
  } | {
116
116
  transcript_id?: undefined;
117
117
  source: string;
@@ -12,6 +12,7 @@ export interface PluginLoadFailure {
12
12
  definition: PluginDefinition;
13
13
  error: unknown;
14
14
  }
15
+ export declare function workerScriptLoadMessage(scriptUrl: string, error: unknown): string;
15
16
  export default class PluginLoader {
16
17
  definitions: PluginDefinition[];
17
18
  fetchESM?: (url: string) => Promise<LoadedPlugin>;
@@ -1,5 +1,6 @@
1
+ import { enableContractReports } from '@jbrowse/render-core/contractReports';
1
2
  import { setReExportRegistry } from './ReExports/registry.js';
2
- import { isCJSPluginDefinition, isESMPluginDefinition, isUMDPluginDefinition, pluginDescriptionString, } from './pluginDefinitions.js';
3
+ import { isCJSPluginDefinition, isESMPluginDefinition, isUMDPluginDefinition, maybePluginUrl, pluginDescriptionString, } from './pluginDefinitions.js';
3
4
  import { isElectron } from './util/index.js';
4
5
  import { isWebWorker } from './util/isWebWorker.js';
5
6
  /**
@@ -45,6 +46,19 @@ function promisifiedLoadScript(src, integrity) {
45
46
  function hasImportScripts(scope) {
46
47
  return 'importScripts' in scope;
47
48
  }
49
+ // A *module* worker has importScripts on its global but throws from it — the
50
+ // spec forbids it there, and the browser says so ("Module scripts don't support
51
+ // importScripts"). That is the whole failure for a UMD plugin in a bundler
52
+ // configured for module workers (Vite's `worker.format: 'es'`), and it reads as
53
+ // a broken plugin unless named. Every other throw is the script's own — a 404,
54
+ // or a bundle that dereferences `window` in a realm that has none — and the
55
+ // script's words are the diagnosis, so they are kept rather than replaced.
56
+ export function workerScriptLoadMessage(scriptUrl, error) {
57
+ const detail = error instanceof Error ? error.message : String(error);
58
+ return /module script/i.test(detail)
59
+ ? `Failed to load ${scriptUrl} in the worker. A UMD plugin is loaded there via importScripts, which module workers do not support — either build the worker as a classic worker, or use a plugin published as ESM.`
60
+ : `Failed to load ${scriptUrl} in the worker: ${detail}`;
61
+ }
48
62
  async function loadScript(scriptUrl, integrity) {
49
63
  const scope = globalThis;
50
64
  if (!isWebWorker()) {
@@ -55,12 +69,9 @@ async function loadScript(scriptUrl, integrity) {
55
69
  scope.importScripts(scriptUrl);
56
70
  }
57
71
  catch (error) {
58
- // A *module* worker has importScripts on its global but throws from it —
59
- // the spec forbids it there. That is the whole failure for a UMD plugin
60
- // in a bundler configured for module workers (Vite's `worker.format:
61
- // 'es'`), and the raw TypeError names none of it, so it reads as a broken
62
- // plugin rather than an incompatible pairing.
63
- throw new Error(`Failed to load ${scriptUrl} in the worker. A UMD plugin is loaded there via importScripts, which module workers do not support — either build the worker as a classic worker, or use a plugin published as ESM.`, { cause: error });
72
+ throw new Error(workerScriptLoadMessage(scriptUrl, error), {
73
+ cause: error,
74
+ });
64
75
  }
65
76
  return;
66
77
  }
@@ -97,6 +108,34 @@ function resolvePluginUrl(spec, base) {
97
108
  }
98
109
  return url;
99
110
  }
111
+ // A plugin served from the developer's own machine into an app built for
112
+ // production is the one arrangement that can only be a plugin under
113
+ // development: a shipped site serves its plugins from its own origin, and a
114
+ // developer running the app itself from source already has a development build.
115
+ // So it is the evidence that arms the contract channel without anything for the
116
+ // author to know about first — which is the whole difficulty the channel had,
117
+ // since the population it is written for is the one with nobody to ask. See
118
+ // `@jbrowse/render-core/contractReports`.
119
+ function armIfUnderDevelopment(def) {
120
+ const url = maybePluginUrl(def);
121
+ if (url !== undefined && servedFromThisMachine(url)) {
122
+ enableContractReports(`the plugin at ${url} is served from this machine`);
123
+ }
124
+ }
125
+ function servedFromThisMachine(url) {
126
+ try {
127
+ const { hostname, protocol } = new URL(url);
128
+ return ((protocol === 'http:' || protocol === 'https:') &&
129
+ (hostname === 'localhost' ||
130
+ hostname === '127.0.0.1' ||
131
+ hostname === '[::1]'));
132
+ }
133
+ catch {
134
+ // a relative url is a plugin shipped beside this index.html, which is the
135
+ // site's own rather than one under development
136
+ return false;
137
+ }
138
+ }
100
139
  function addCacheBuster(url) {
101
140
  if (!('__jbrowseCacheBuster' in globalThis)) {
102
141
  return url;
@@ -155,6 +194,7 @@ export default class PluginLoader {
155
194
  }
156
195
  async loadPlugin(def, baseUri) {
157
196
  assertSingleKind(def);
197
+ armIfUnderDevelopment(def);
158
198
  let plugin;
159
199
  if (isCJSPluginDefinition(def)) {
160
200
  if (!isElectron) {
@@ -198,7 +238,11 @@ export default class PluginLoader {
198
238
  if (!target) {
199
239
  return;
200
240
  }
201
- const { default: ReExports } = await import('./ReExports/index.js');
241
+ // a worker never renders: the same keys, with the UI ones stubbed, so a
242
+ // plugin bundle evaluates there without fetching react-dom and Material UI
243
+ const { default: ReExports } = isWebWorker()
244
+ ? await import('./ReExports/workerModules.js')
245
+ : await import('./ReExports/index.js');
202
246
  target.JBrowseExports = {
203
247
  ...ReExports,
204
248
  };
@@ -20,7 +20,7 @@ import './ui/buildExtraTrackMenuItems.ts';
20
20
  import './ui/multiTrackMenuItems.ts';
21
21
  import './util/addTrackComponent.ts';
22
22
  import type { AbstractRootModel, AbstractSessionModel, SimpleFeatureSerialized } from './util/index.ts';
23
- import type { IAnyModelType, IStateTreeNode, IAnyType } from '@jbrowse/mobx-state-tree';
23
+ import type { IAnyModelType, IStateTreeNode, IAnyType, SnapshotIn } from '@jbrowse/mobx-state-tree';
24
24
  import type { ComponentType, ReactNode } from 'react';
25
25
  /**
26
26
  * Every pluggable element group, in the order `elementCreationSchedule` builds
@@ -137,7 +137,18 @@ export interface ReplaceWidgetProps {
137
137
  *
138
138
  * That is what makes `extendViewType(pm, 'LinearGenomeView', …)` hand back a
139
139
  * typed state model, and a misspelled or renamed name a compile error rather
140
- * than an extension that quietly never applies. Declaring them here rather than
140
+ * than an extension that quietly never applies. `getViewType` reads it too, so
141
+ * a caller composing another view's model into its own gets the real type
142
+ * instead of `IAnyModelType`.
143
+ *
144
+ * AN AUGMENTATION IS HAND-WRITTEN, so each registration annotates the model it
145
+ * passes to `ViewType` against its own entry here (`const stateModel:
146
+ * ViewTypeRegistry['CircularView'] = …`). That one line is what makes the
147
+ * registry earn what `getViewType` promises: without it a drifted entry is
148
+ * worse than the `IAnyModelType` it replaced, since a caller would get a
149
+ * confidently wrong type rather than an unchecked one. It cannot live on
150
+ * `ViewType`'s constructor — TypeScript forbids type parameters there, and a
151
+ * class parameter widens `name` to `string`, so the conditional never fires. Declaring them here rather than
141
152
  * beside the helpers is load-bearing: an interface can only be augmented
142
153
  * through the module that declares it, and a barrel re-exporting the type would
143
154
  * give every augmentation its own unrelated copy.
@@ -148,6 +159,21 @@ export interface DisplayTypeRegistry {
148
159
  }
149
160
  export type ViewTypeName = keyof ViewTypeRegistry;
150
161
  export type DisplayTypeName = keyof DisplayTypeRegistry;
162
+ /**
163
+ * What `addView(name, snapshot)` takes for a view type name. A registered name
164
+ * gets the view's authored shape — the snapshot type `withLaunchInput` widened
165
+ * to accept the launch keys beside the declared properties — so a misspelled
166
+ * key is a compile error at the literal site. `type` is optional because
167
+ * `addView` writes it. An unregistered name stays `Record<string, unknown>`,
168
+ * which is what an out-of-tree view's `addView` has always been.
169
+ *
170
+ * Read off `ViewTypeRegistry` rather than registered a second time, so the
171
+ * snapshot type cannot drift from the model: a view that augments the registry
172
+ * has typed `addView`, `replaceView` and `addOrReplaceView` for free.
173
+ */
174
+ export type ViewSnapshotInput<N extends string> = N extends ViewTypeName ? Omit<SnapshotIn<ViewTypeRegistry[N]>, 'type'> & {
175
+ type?: N;
176
+ } : Record<string, unknown>;
151
177
  export interface ExtensionPointRegistry {
152
178
  'Core-extendSession': {
153
179
  args: IAnyModelType;
@@ -268,6 +294,7 @@ export interface RuntimePluginLoadRecord extends PluginLoadRecord {
268
294
  definition: PluginDefinition;
269
295
  }
270
296
  export default class PluginManager {
297
+ #private;
271
298
  plugins: Plugin[];
272
299
  jexl: import("@jbrowse/jexl").Jexl;
273
300
  pluginMetadata: Record<string, PluginMetadata>;
@@ -289,25 +316,27 @@ export default class PluginManager {
289
316
  extensionPoints: Map<string, ExtensionPointCallback[]>;
290
317
  warnedClobber: Set<string>;
291
318
  /**
292
- * Lazy-hydration cache for `TrackConfigurationReference`/
293
- * `DisplayConfigurationReference` (configuration/configurationSchema.ts).
319
+ * The lazily-hydrated MST config node for one frozen track config, built by
320
+ * `build` on first ask and returned by identity after that.
321
+ * `configuration/configurationSchema.ts` is the only caller.
322
+ *
294
323
  * `jbrowse.tracks` is `types.frozen` for large-tracklist performance, so a
295
- * track config is a plain JS object until first referenced; hydrating it
296
- * into an MST node is deferred to that read. MST's custom-reference
297
- * `getValue` has no memoization of its own — it reruns on every property
298
- * access — so without this cache, every read of `track.configuration` would
299
- * fabricate a fresh, non-identical MST node. Keyed by schemaType (each track
300
- * type's config schema is rebuilt fresh per PluginManager instance, see
301
- * addTrackType) then by the frozen object itself, so a cache hit can only
302
- * ever come from this same PluginManager instance and this same track type.
303
- * See ADR-031.
324
+ * track config is a plain JS object until first referenced; hydrating it into
325
+ * an MST node is deferred to that read. MST's custom-reference `getValue` has
326
+ * no memoization of its own — it reruns on every property access — so without
327
+ * this, every read of `track.configuration` would fabricate a fresh,
328
+ * non-identical node. Keyed by schemaType (each track type's config schema is
329
+ * rebuilt fresh per PluginManager instance, see addTrackType) then by the
330
+ * frozen object, so a hit can only ever come from this same instance and this
331
+ * same track type. See ADR-031.
304
332
  *
305
- * This node is never mutated: admin edits replace the frozen entry (new
333
+ * The node is never mutated: admin edits replace the frozen entry (new
306
334
  * identity drops the WeakMap entry), and a non-admin's edits go to a private
307
335
  * session working copy, not here (ADR-032). Both levels are `WeakMap`s so
308
- * entries collect normally — no manual invalidation needed.
336
+ * entries collect normally — no manual invalidation needed, which is why this
337
+ * is a memo and not a cache with a lifecycle to expose.
309
338
  */
310
- trackConfigHydrationCache: WeakMap<object, WeakMap<object, unknown>>;
339
+ hydratedTrackConfig(schemaType: IAnyType, frozen: object, build: () => unknown): unknown;
311
340
  constructor(initialPlugins?: (Plugin | PluginLoadRecord | RuntimePluginLoadRecord)[]);
312
341
  pluginConfigurationNamespacedSchemas(): Record<string, unknown>;
313
342
  pluginConfigurationUnnamespacedSchemas(): Record<string, unknown>;
@@ -330,7 +359,11 @@ export default class PluginManager {
330
359
  getRpcElements(): RpcMethodType[];
331
360
  getDisplayElements(): DisplayType[];
332
361
  getAdapterElements(): AdapterType[];
333
- /** get a MST type for the union of all specified pluggable MST types */
362
+ /**
363
+ * get a MST type for the union of all specified pluggable MST types.
364
+ * Membership is late-bound, so a lazily registered state model joins when
365
+ * its loader resolves; a snapshot naming one cannot be cast until then.
366
+ */
334
367
  pluggableMstType(groupName: PluggableElementTypeGroup, fieldName: string, fallback?: IAnyType): IAnyType;
335
368
  /** get a MST type for the union of all specified pluggable config schemas */
336
369
  pluggableConfigSchemaType(typeGroup: PluggableElementTypeGroup, fieldName?: string): IAnyType;
@@ -357,7 +390,27 @@ export default class PluginManager {
357
390
  getTextSearchAdapterType(typeName: string): TextSearchAdapterType;
358
391
  getTrackType(typeName: string): TrackType;
359
392
  getDisplayType(typeName: string): DisplayType;
393
+ getViewType<N extends ViewTypeName>(typeName: N): Omit<ViewType, 'stateModel'> & {
394
+ stateModel: ViewTypeRegistry[N];
395
+ };
360
396
  getViewType(typeName: string): ViewType;
397
+ private sessionTypeNames;
398
+ /**
399
+ * Every async code path that hands a snapshot to a synchronous session
400
+ * instantiation (`setSession`, `cast`) must await this first.
401
+ */
402
+ preloadSessionTypes(sessionSnapshot: unknown): Promise<void>;
403
+ unloadedSessionTypes(sessionSnapshot: unknown): string[];
404
+ /**
405
+ * Without this, a snapshot naming an unloaded lazy type fails inside MST as
406
+ * a union mismatch that reads like a corrupt snapshot.
407
+ */
408
+ assertSessionTypesLoaded(sessionSnapshot: unknown): void;
409
+ /**
410
+ * Aliases resolve here because a legacy session names the alias, and the
411
+ * model that remaps it cannot be consulted before it is loaded.
412
+ */
413
+ resolveDisplayTypeRecord(name: string): DisplayType | undefined;
361
414
  getAddTrackWorkflow(typeName: string): AddTrackWorkflowType;
362
415
  getWidgetType(typeName: string): WidgetType;
363
416
  getConnectionType(typeName: string): ConnectionType;
@@ -113,26 +113,41 @@ export default class PluginManager {
113
113
  // singular component points are re-evaluated on every render, so the
114
114
  // clobber warning is emitted once per point per app
115
115
  warnedClobber = new Set();
116
+ #trackConfigHydrationCache = new WeakMap();
116
117
  /**
117
- * Lazy-hydration cache for `TrackConfigurationReference`/
118
- * `DisplayConfigurationReference` (configuration/configurationSchema.ts).
118
+ * The lazily-hydrated MST config node for one frozen track config, built by
119
+ * `build` on first ask and returned by identity after that.
120
+ * `configuration/configurationSchema.ts` is the only caller.
121
+ *
119
122
  * `jbrowse.tracks` is `types.frozen` for large-tracklist performance, so a
120
- * track config is a plain JS object until first referenced; hydrating it
121
- * into an MST node is deferred to that read. MST's custom-reference
122
- * `getValue` has no memoization of its own — it reruns on every property
123
- * access — so without this cache, every read of `track.configuration` would
124
- * fabricate a fresh, non-identical MST node. Keyed by schemaType (each track
125
- * type's config schema is rebuilt fresh per PluginManager instance, see
126
- * addTrackType) then by the frozen object itself, so a cache hit can only
127
- * ever come from this same PluginManager instance and this same track type.
128
- * See ADR-031.
123
+ * track config is a plain JS object until first referenced; hydrating it into
124
+ * an MST node is deferred to that read. MST's custom-reference `getValue` has
125
+ * no memoization of its own — it reruns on every property access — so without
126
+ * this, every read of `track.configuration` would fabricate a fresh,
127
+ * non-identical node. Keyed by schemaType (each track type's config schema is
128
+ * rebuilt fresh per PluginManager instance, see addTrackType) then by the
129
+ * frozen object, so a hit can only ever come from this same instance and this
130
+ * same track type. See ADR-031.
129
131
  *
130
- * This node is never mutated: admin edits replace the frozen entry (new
132
+ * The node is never mutated: admin edits replace the frozen entry (new
131
133
  * identity drops the WeakMap entry), and a non-admin's edits go to a private
132
134
  * session working copy, not here (ADR-032). Both levels are `WeakMap`s so
133
- * entries collect normally — no manual invalidation needed.
135
+ * entries collect normally — no manual invalidation needed, which is why this
136
+ * is a memo and not a cache with a lifecycle to expose.
134
137
  */
135
- trackConfigHydrationCache = new WeakMap();
138
+ hydratedTrackConfig(schemaType, frozen, build) {
139
+ let byFrozen = this.#trackConfigHydrationCache.get(schemaType);
140
+ if (!byFrozen) {
141
+ byFrozen = new WeakMap();
142
+ this.#trackConfigHydrationCache.set(schemaType, byFrozen);
143
+ }
144
+ let node = byFrozen.get(frozen);
145
+ if (!node) {
146
+ node = build();
147
+ byFrozen.set(frozen, node);
148
+ }
149
+ return node;
150
+ }
136
151
  constructor(initialPlugins = []) {
137
152
  // add the core plugin
138
153
  this.addPlugin({
@@ -326,16 +341,36 @@ export default class PluginManager {
326
341
  getAdapterElements() {
327
342
  return this.getElementTypesInGroup('adapter');
328
343
  }
329
- /** get a MST type for the union of all specified pluggable MST types */
344
+ /**
345
+ * get a MST type for the union of all specified pluggable MST types.
346
+ * Membership is late-bound, so a lazily registered state model joins when
347
+ * its loader resolves; a snapshot naming one cannot be cast until then.
348
+ */
330
349
  pluggableMstType(groupName, fieldName, fallback = types.maybe(types.null)) {
331
- const pluggableTypes = this.getElementTypeRecord(groupName)
350
+ const record = this.getElementTypeRecord(groupName);
351
+ const anyMember = record.all().some(t => {
352
+ const element = t;
353
+ const field = element[fieldName];
354
+ return ((isType(field) && isModelType(field)) ||
355
+ typeof element[`${fieldName}Loader`] === 'function');
356
+ });
357
+ if (!anyMember) {
358
+ return fallback;
359
+ }
360
+ const modelsOf = () => record
332
361
  .all()
333
362
  .map(t => t[fieldName])
334
363
  .filter(t => isType(t) && isModelType(t));
335
- if (pluggableTypes.length === 0) {
336
- return fallback;
337
- }
338
- return types.union(...pluggableTypes);
364
+ let members = modelsOf();
365
+ return types.union({
366
+ name: `pluggable(${groupName} ${fieldName})`,
367
+ members: () => {
368
+ if (members.length !== modelsOf().length) {
369
+ members = modelsOf();
370
+ }
371
+ return members;
372
+ },
373
+ });
339
374
  }
340
375
  /** get a MST type for the union of all specified pluggable config schemas */
341
376
  pluggableConfigSchemaType(typeGroup, fieldName = 'configSchema') {
@@ -422,6 +457,102 @@ export default class PluginManager {
422
457
  getViewType(typeName) {
423
458
  return this.viewTypes.get(typeName);
424
459
  }
460
+ sessionTypeNames(sessionSnapshot) {
461
+ const viewNames = new Set();
462
+ const displayNames = new Set();
463
+ const collectTracks = (tracks) => {
464
+ if (Array.isArray(tracks)) {
465
+ for (const track of tracks) {
466
+ const { displays } = track && typeof track === 'object'
467
+ ? track
468
+ : {};
469
+ if (Array.isArray(displays)) {
470
+ for (const display of displays) {
471
+ const type = display && typeof display === 'object'
472
+ ? display.type
473
+ : undefined;
474
+ if (typeof type === 'string') {
475
+ displayNames.add(type);
476
+ }
477
+ }
478
+ }
479
+ }
480
+ }
481
+ };
482
+ const collectViews = (views) => {
483
+ if (Array.isArray(views)) {
484
+ for (const view of views) {
485
+ if (view && typeof view === 'object') {
486
+ const { type, views: children, tracks, levels, } = view;
487
+ if (typeof type === 'string') {
488
+ viewNames.add(type);
489
+ }
490
+ collectViews(children);
491
+ collectTracks(tracks);
492
+ if (Array.isArray(levels)) {
493
+ for (const level of levels) {
494
+ collectTracks(level && typeof level === 'object'
495
+ ? level.tracks
496
+ : undefined);
497
+ }
498
+ }
499
+ }
500
+ }
501
+ }
502
+ };
503
+ if (sessionSnapshot && typeof sessionSnapshot === 'object') {
504
+ const { view, views } = sessionSnapshot;
505
+ collectViews(views);
506
+ collectViews(view ? [view] : undefined);
507
+ }
508
+ return { viewNames, displayNames };
509
+ }
510
+ /**
511
+ * Every async code path that hands a snapshot to a synchronous session
512
+ * instantiation (`setSession`, `cast`) must await this first.
513
+ */
514
+ async preloadSessionTypes(sessionSnapshot) {
515
+ const { viewNames, displayNames } = this.sessionTypeNames(sessionSnapshot);
516
+ await Promise.all([
517
+ ...[...viewNames]
518
+ .filter(name => this.viewTypes.has(name))
519
+ .map(name => this.getViewType(name).loadStateModel()),
520
+ ...[...displayNames]
521
+ .map(name => this.resolveDisplayTypeRecord(name))
522
+ .flatMap(display => (display ? [display.loadStateModel()] : [])),
523
+ ]);
524
+ }
525
+ unloadedSessionTypes(sessionSnapshot) {
526
+ const { viewNames, displayNames } = this.sessionTypeNames(sessionSnapshot);
527
+ return [
528
+ ...[...viewNames].filter(name => this.viewTypes.has(name) &&
529
+ !this.getViewType(name).isStateModelLoaded),
530
+ ...[...displayNames].filter(name => {
531
+ const display = this.resolveDisplayTypeRecord(name);
532
+ return display !== undefined && !display.isStateModelLoaded;
533
+ }),
534
+ ];
535
+ }
536
+ /**
537
+ * Without this, a snapshot naming an unloaded lazy type fails inside MST as
538
+ * a union mismatch that reads like a corrupt snapshot.
539
+ */
540
+ assertSessionTypesLoaded(sessionSnapshot) {
541
+ const unloaded = this.unloadedSessionTypes(sessionSnapshot);
542
+ if (unloaded.length > 0) {
543
+ throw new Error(`session names lazily loaded types that are not loaded yet: ${unloaded.join(', ')}. Await pluginManager.preloadSessionTypes(snapshot) before setting the session`);
544
+ }
545
+ }
546
+ /**
547
+ * Aliases resolve here because a legacy session names the alias, and the
548
+ * model that remaps it cannot be consulted before it is loaded.
549
+ */
550
+ resolveDisplayTypeRecord(name) {
551
+ if (this.displayTypes.has(name)) {
552
+ return this.getDisplayType(name);
553
+ }
554
+ return this.getDisplayElements().find(d => d.aliases?.includes(name));
555
+ }
425
556
  getAddTrackWorkflow(typeName) {
426
557
  return this.addTrackWidgets.get(typeName);
427
558
  }