@jbrowse/product-core 5.0.0-beta.1 → 5.0.0-beta.3

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 (53) hide show
  1. package/esm/RootModel/BaseRootModel.js +2 -1
  2. package/esm/RootModel/PreferencesConfig.d.ts +15 -0
  3. package/esm/RootModel/PreferencesConfig.js +15 -0
  4. package/esm/RootModel/createConfigModel.d.ts +8 -0
  5. package/esm/Session/BaseSession.d.ts +11 -26
  6. package/esm/Session/BaseSession.js +21 -60
  7. package/esm/Session/MultipleViews.d.ts +17 -3
  8. package/esm/Session/MultipleViews.js +45 -5
  9. package/esm/Session/Preferences.d.ts +2 -1
  10. package/esm/Session/Preferences.js +5 -0
  11. package/esm/Session/SessionTracks.d.ts +4 -1
  12. package/esm/Session/SessionTracks.js +8 -0
  13. package/esm/Session/TrackMenu.js +2 -2
  14. package/esm/Session/Tracks.d.ts +4 -5
  15. package/esm/Session/Tracks.js +115 -92
  16. package/esm/Session/developerMode.d.ts +7 -0
  17. package/esm/Session/developerMode.js +43 -0
  18. package/esm/Session/readdedTrackConf.d.ts +8 -0
  19. package/esm/Session/readdedTrackConf.js +25 -0
  20. package/esm/Session/temporaryAssemblyTracks.d.ts +4 -3
  21. package/esm/Session/temporaryAssemblyTracks.js +6 -7
  22. package/esm/controllerTracks.d.ts +3 -3
  23. package/esm/controllerTracks.js +17 -4
  24. package/esm/index.d.ts +2 -2
  25. package/esm/index.js +2 -2
  26. package/esm/loadPlugins.d.ts +1 -1
  27. package/esm/loadPlugins.js +6 -3
  28. package/esm/pruneUnbuildableNodes.js +5 -2
  29. package/esm/rpcWorker.js +5 -4
  30. package/esm/ui/AboutDialogContents.js +2 -1
  31. package/esm/ui/DescriptionPanel.d.ts +3 -0
  32. package/esm/ui/DescriptionPanel.js +24 -0
  33. package/esm/ui/DisplayDefaultsSection.d.ts +7 -20
  34. package/esm/ui/DisplayDefaultsSection.js +9 -47
  35. package/esm/ui/GeneralPreferencesTab.d.ts +14 -0
  36. package/esm/ui/GeneralPreferencesTab.js +27 -0
  37. package/esm/ui/PreferenceCheckbox.d.ts +6 -0
  38. package/esm/ui/PreferenceCheckbox.js +16 -0
  39. package/esm/ui/PreferencesDialog.d.ts +6 -27
  40. package/esm/ui/PreferencesDialog.js +38 -119
  41. package/esm/ui/ViewPreferencesTab.d.ts +12 -0
  42. package/esm/ui/ViewPreferencesTab.js +15 -0
  43. package/esm/ui/descriptionHtml.d.ts +26 -0
  44. package/esm/ui/descriptionHtml.js +66 -0
  45. package/esm/ui/displayDefaultChanges.d.ts +31 -0
  46. package/esm/ui/displayDefaultChanges.js +66 -0
  47. package/esm/ui/preferencesReset.d.ts +18 -0
  48. package/esm/ui/preferencesReset.js +64 -0
  49. package/esm/useEngineLifecycle.d.ts +7 -1
  50. package/esm/useEngineLifecycle.js +24 -2
  51. package/package.json +5 -4
  52. package/esm/scheduleDetachedDestroy.d.ts +0 -18
  53. package/esm/scheduleDetachedDestroy.js +0 -24
@@ -1,9 +1,9 @@
1
1
  import TextSearchManager from '@jbrowse/core/TextSearch/TextSearchManager';
2
2
  import assemblyManagerFactory from '@jbrowse/core/assemblyManager';
3
3
  import RpcManager from '@jbrowse/core/rpc/RpcManager';
4
+ import { scheduleDetachedDestroy } from '@jbrowse/core/util';
4
5
  import { cast, detach, getType, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
5
6
  import { describeUnbuildableNodes, pruneUnbuildableNodes, } from '../pruneUnbuildableNodes.js';
6
- import { scheduleDetachedDestroy } from '../scheduleDetachedDestroy.js';
7
7
  import { migrateSessionSnapshot } from '../sessionMigrations/index.js';
8
8
  import { filterSessionInPlace } from '../sessionUtils.js';
9
9
  // `session` is a `types.maybe(sessionModelType)` where `sessionModelType` is the
@@ -87,6 +87,7 @@ export function BaseRootModelFactory({ pluginManager, jbrowseModelType, sessionM
87
87
  const unbuildable = [];
88
88
  let migrated = sessionSnapshot;
89
89
  if (sessionSnapshot && typeof sessionSnapshot === 'object') {
90
+ pluginManager.assertSessionTypesLoaded(sessionSnapshot);
90
91
  // before the cast, because a type this build has no plugin for is a
91
92
  // union failure the try below never gets to see
92
93
  const pruned = pruneUnbuildableNodes(migrateSessionSnapshot(sessionSnapshot), pluginManager, sessionModelType);
@@ -50,4 +50,19 @@ export declare function PreferencesConfigSchemaFactory(): import("@jbrowse/core/
50
50
  readonly type: "boolean";
51
51
  readonly defaultValue: false;
52
52
  };
53
+ /**
54
+ * #slot configuration.preferences.developerMode
55
+ * when true, the ordering-contract checks a display or plugin can break
56
+ * report themselves in the app rather than only in a development build.
57
+ * Off by default and deliberately not in the Preferences dialog: the
58
+ * messages are about code, and a reader who cannot change the code cannot
59
+ * act on one. Turn it on for a site that is running a plugin under
60
+ * development; a plugin author needs nothing here, since a plugin served
61
+ * from localhost arms the same channel on its own, as does
62
+ * `localStorage.jbrowseDeveloperMode`. Takes effect on reload.
63
+ */
64
+ readonly developerMode: {
65
+ readonly type: "boolean";
66
+ readonly defaultValue: false;
67
+ };
53
68
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
@@ -57,5 +57,20 @@ export function PreferencesConfigSchemaFactory() {
57
57
  type: 'boolean',
58
58
  defaultValue: false,
59
59
  },
60
+ /**
61
+ * #slot configuration.preferences.developerMode
62
+ * when true, the ordering-contract checks a display or plugin can break
63
+ * report themselves in the app rather than only in a development build.
64
+ * Off by default and deliberately not in the Preferences dialog: the
65
+ * messages are about code, and a reader who cannot change the code cannot
66
+ * act on one. Turn it on for a site that is running a plugin under
67
+ * development; a plugin author needs nothing here, since a plugin served
68
+ * from localhost arms the same channel on its own, as does
69
+ * `localStorage.jbrowseDeveloperMode`. Takes effect on reload.
70
+ */
71
+ developerMode: {
72
+ type: 'boolean',
73
+ defaultValue: false,
74
+ },
60
75
  });
61
76
  }
@@ -66,6 +66,10 @@ declare function rootConfigurationSchema(): import("@jbrowse/core/configuration/
66
66
  readonly type: "boolean";
67
67
  readonly defaultValue: false;
68
68
  };
69
+ readonly developerMode: {
70
+ readonly type: "boolean";
71
+ readonly defaultValue: false;
72
+ };
69
73
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
70
74
  readonly formatDetails: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
71
75
  readonly feature: {
@@ -178,6 +182,10 @@ export declare function createConfigModel(pluginManager: PluginManager, assembly
178
182
  readonly type: "boolean";
179
183
  readonly defaultValue: false;
180
184
  };
185
+ readonly developerMode: {
186
+ readonly type: "boolean";
187
+ readonly defaultValue: false;
188
+ };
181
189
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
182
190
  readonly formatDetails: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
183
191
  readonly feature: {
@@ -162,11 +162,8 @@ export declare function BaseSessionModel<ROOT_MODEL_TYPE extends BaseRootModelTy
162
162
  * `{ displayType, slot, value }` — the inventory the Preferences dialog
163
163
  * lists and clears one at a time (`setDisplayTypeDefault(…, undefined)`).
164
164
  *
165
- * Here rather than filtered out of `getPreferenceChanges` by the dialog,
166
- * because the composite-key layout is this file's: a consumer that
167
- * recognized these rows by matching the path head is exactly the coupling
168
- * `DISPLAY_TYPE_DEFAULTS_PATH_HEAD` stopped being exported over, where a
169
- * rename on one side alone silently no-ops the other.
165
+ * Parsed here rather than by the dialog, because the composite-key
166
+ * layout is this file's and nothing outside it should depend on it.
170
167
  */
171
168
  getDisplayTypeDefaults(): {
172
169
  displayType: string;
@@ -175,14 +172,13 @@ export declare function BaseSessionModel<ROOT_MODEL_TYPE extends BaseRootModelTy
175
172
  }[];
176
173
  /**
177
174
  * #method
178
- * every runtime preference-override that currently differs from its
179
- * config/admin default, as `{ path, from, to }` rows — the exact set
180
- * `clearPreferenceOverrides` reverts. Backs the confirmation diff shown
181
- * before "Reset to defaults" (mirrors the per-track changes dialog). A
182
- * scalar pref (animationMode, scrollZoom) whose override equals the
183
- * default is omitted (reverting it is a no-op); each promoted
184
- * per-display-type default is always a difference from the un-promoted
185
- * state, so `from` reads "(default)".
175
+ * every scalar preference override that currently differs from its
176
+ * config/admin default, as `{ path, from, to }` rows whose path is the
177
+ * override's own key. A scalar pref (animationMode, scrollZoom) whose
178
+ * override equals the default is omitted, since reverting it is a no-op.
179
+ * Promoted display-type defaults share the map but are not here: they
180
+ * are `getDisplayTypeDefaults`, and the Preferences dialog lists them
181
+ * through one builder on both of its surfaces.
186
182
  */
187
183
  getPreferenceChanges(): TrackConfigChange[];
188
184
  } & {
@@ -261,19 +257,6 @@ export declare function BaseSessionModel<ROOT_MODEL_TYPE extends BaseRootModelTy
261
257
  * Preferences dialog "Reset to defaults" confirmation.
262
258
  */
263
259
  clearPreferenceOverride(key: string): void;
264
- /**
265
- * #action
266
- * revert one row emitted by `getPreferenceChanges`, addressed by its
267
- * display `path`. Backs the per-entry reset in the Preferences dialog's
268
- * "Reset to defaults" confirmation.
269
- *
270
- * Lives here rather than in that dialog because this model owns both path
271
- * shapes it has to undo: a promoted per-display-type default, whose row
272
- * path is a readable label over a flat composite storage key, and every
273
- * other override, whose path *is* its key. The dialog used to re-derive
274
- * the first case from an exported path-head constant.
275
- */
276
- resetPreferenceChange(path: string[]): void;
277
260
  /**
278
261
  * #action
279
262
  * set the global scroll-to-zoom preference (see the `scrollZoom` getter)
@@ -322,6 +305,8 @@ export declare function BaseSessionModel<ROOT_MODEL_TYPE extends BaseRootModelTy
322
305
  pushSnackbarMessage(message: string, level?: import("@jbrowse/core/util").NotificationLevel, actions?: import("@jbrowse/core/util").SnackAction[]): void;
323
306
  popSnackbarMessage(): import("@jbrowse/core/ui/SnackbarModel").SnackbarMessage | undefined;
324
307
  removeSnackbarMessage(message: string): void;
308
+ } & {
309
+ afterAttach(): void;
325
310
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
326
311
  /** Session mixin MST type for the most basic session */
327
312
  export type BaseSessionType = ReturnType<typeof BaseSessionModel>;
@@ -6,6 +6,7 @@ import { isFeature, unwrapFeature } from '@jbrowse/core/util/simpleFeature';
6
6
  import { ElementId } from '@jbrowse/core/util/types/mst';
7
7
  import { getParent, isStateTreeNode, types } from '@jbrowse/mobx-state-tree';
8
8
  import { observable } from 'mobx';
9
+ import { applyDeveloperMode } from './developerMode.js';
9
10
  function isAnimationMode(val) {
10
11
  return val === 'system' || val === 'enabled' || val === 'disabled';
11
12
  }
@@ -38,18 +39,6 @@ function parseDisplayTypeDefaultKey(key) {
38
39
  const [displayType, slot] = rest.split('\0');
39
40
  return displayType && slot ? { displayType, slot } : undefined;
40
41
  }
41
- // Head of the display path a promoted default takes in a `getPreferenceChanges`
42
- // row: `[DISPLAY_TYPE_DEFAULTS_PATH_HEAD, displayType, slot]`. Purely a
43
- // readable row label, deliberately distinct from the flat
44
- // `DISPLAY_TYPE_DEFAULT_PREFIX` storage key above.
45
- //
46
- // Module-private, like the storage key: both the producer
47
- // (`getPreferenceChanges`) and the consumer (`resetPreferenceChange`) are
48
- // methods on this model, so the two path shapes this file emits are the only
49
- // ones it has to undo. It used to be exported for the Preferences dialog to
50
- // match on and re-route back through `setDisplayTypeDefault`, which meant a
51
- // rename on one side alone would silently no-op that dialog's per-row reset.
52
- const DISPLAY_TYPE_DEFAULTS_PATH_HEAD = 'displayTypeDefaults';
53
42
  /**
54
43
  * #stateModel BaseSessionModel
55
44
  *
@@ -244,11 +233,8 @@ export function BaseSessionModel(_pluginManager) {
244
233
  * `{ displayType, slot, value }` — the inventory the Preferences dialog
245
234
  * lists and clears one at a time (`setDisplayTypeDefault(…, undefined)`).
246
235
  *
247
- * Here rather than filtered out of `getPreferenceChanges` by the dialog,
248
- * because the composite-key layout is this file's: a consumer that
249
- * recognized these rows by matching the path head is exactly the coupling
250
- * `DISPLAY_TYPE_DEFAULTS_PATH_HEAD` stopped being exported over, where a
251
- * rename on one side alone silently no-ops the other.
236
+ * Parsed here rather than by the dialog, because the composite-key
237
+ * layout is this file's and nothing outside it should depend on it.
252
238
  */
253
239
  getDisplayTypeDefaults() {
254
240
  const found = [];
@@ -262,31 +248,18 @@ export function BaseSessionModel(_pluginManager) {
262
248
  },
263
249
  /**
264
250
  * #method
265
- * every runtime preference-override that currently differs from its
266
- * config/admin default, as `{ path, from, to }` rows — the exact set
267
- * `clearPreferenceOverrides` reverts. Backs the confirmation diff shown
268
- * before "Reset to defaults" (mirrors the per-track changes dialog). A
269
- * scalar pref (animationMode, scrollZoom) whose override equals the
270
- * default is omitted (reverting it is a no-op); each promoted
271
- * per-display-type default is always a difference from the un-promoted
272
- * state, so `from` reads "(default)".
251
+ * every scalar preference override that currently differs from its
252
+ * config/admin default, as `{ path, from, to }` rows whose path is the
253
+ * override's own key. A scalar pref (animationMode, scrollZoom) whose
254
+ * override equals the default is omitted, since reverting it is a no-op.
255
+ * Promoted display-type defaults share the map but are not here: they
256
+ * are `getDisplayTypeDefaults`, and the Preferences dialog lists them
257
+ * through one builder on both of its surfaces.
273
258
  */
274
259
  getPreferenceChanges() {
275
260
  const changes = [];
276
261
  for (const [key, value] of self.preferencesOverrides.entries()) {
277
- const dtd = parseDisplayTypeDefaultKey(key);
278
- if (dtd) {
279
- changes.push({
280
- path: [
281
- DISPLAY_TYPE_DEFAULTS_PATH_HEAD,
282
- dtd.displayType,
283
- dtd.slot,
284
- ],
285
- from: undefined,
286
- to: value,
287
- });
288
- }
289
- else {
262
+ if (!key.startsWith(DISPLAY_TYPE_DEFAULT_PREFIX)) {
290
263
  const dflt = this.getPreferenceDefault(key);
291
264
  if (value !== dflt) {
292
265
  changes.push({
@@ -417,27 +390,6 @@ export function BaseSessionModel(_pluginManager) {
417
390
  clearPreferenceOverride(key) {
418
391
  self.preferencesOverrides.delete(key);
419
392
  },
420
- /**
421
- * #action
422
- * revert one row emitted by `getPreferenceChanges`, addressed by its
423
- * display `path`. Backs the per-entry reset in the Preferences dialog's
424
- * "Reset to defaults" confirmation.
425
- *
426
- * Lives here rather than in that dialog because this model owns both path
427
- * shapes it has to undo: a promoted per-display-type default, whose row
428
- * path is a readable label over a flat composite storage key, and every
429
- * other override, whose path *is* its key. The dialog used to re-derive
430
- * the first case from an exported path-head constant.
431
- */
432
- resetPreferenceChange(path) {
433
- const [head, displayType, slot] = path;
434
- if (head === DISPLAY_TYPE_DEFAULTS_PATH_HEAD && displayType && slot) {
435
- this.setDisplayTypeDefault(displayType, slot, undefined);
436
- }
437
- else if (head) {
438
- this.clearPreferenceOverride(head);
439
- }
440
- },
441
393
  /**
442
394
  * #action
443
395
  * set the global scroll-to-zoom preference (see the `scrollZoom` getter)
@@ -490,7 +442,16 @@ export function BaseSessionModel(_pluginManager) {
490
442
  self.queueOfDialogs = [...self.queueOfDialogs, [component, props]];
491
443
  },
492
444
  }));
493
- return types.compose(baseModel, SnackbarModel());
445
+ return types.compose(baseModel, SnackbarModel()).actions(self => ({
446
+ // after the compose rather than in the afterAttach above, because the
447
+ // notices land in the snackbar this line is the first place to hold both
448
+ // halves of. Web and desktop reach it a second time once the user's stored
449
+ // overrides are loaded (`PreferencesSessionMixin`), and our MST fork
450
+ // auto-chains both hooks.
451
+ afterAttach() {
452
+ applyDeveloperMode(self);
453
+ },
454
+ }));
494
455
  }
495
456
  /** Type guard for BaseSession */
496
457
  export function isBaseSession(thing) {
@@ -1,4 +1,5 @@
1
1
  import type PluginManager from '@jbrowse/core/PluginManager';
2
+ import type { ViewSnapshotInput } from '@jbrowse/core/PluginManager';
2
3
  import type { IBaseViewModel } from '@jbrowse/core/pluggableElementTypes';
3
4
  import type { IAnyStateTreeNode, Instance } from '@jbrowse/mobx-state-tree';
4
5
  /**
@@ -1659,7 +1660,6 @@ export declare function MultipleViewsSessionMixin(pluginManager: PluginManager):
1659
1660
  setPreferenceOverride(key: string, value: unknown): void;
1660
1661
  clearPreferenceOverrides(): void;
1661
1662
  clearPreferenceOverride(key: string): void;
1662
- resetPreferenceChange(path: string[]): void;
1663
1663
  setScrollZoom(flag: boolean): void;
1664
1664
  setDisplayTypeDefault(displayType: string, slot: string, value: unknown): void;
1665
1665
  setName(str: string): void;
@@ -1678,6 +1678,8 @@ export declare function MultipleViewsSessionMixin(pluginManager: PluginManager):
1678
1678
  pushSnackbarMessage(message: string, level?: import("@jbrowse/core/util").NotificationLevel, actions?: import("@jbrowse/core/util").SnackAction[]): void;
1679
1679
  popSnackbarMessage(): import("@jbrowse/core/ui/SnackbarModel").SnackbarMessage | undefined;
1680
1680
  removeSnackbarMessage(message: string): void;
1681
+ } & {
1682
+ afterAttach(): void;
1681
1683
  } & {
1682
1684
  readonly visibleWidget: import("@jbrowse/core/util").Widget | undefined;
1683
1685
  } & {
@@ -1752,8 +1754,10 @@ export declare function MultipleViewsSessionMixin(pluginManager: PluginManager):
1752
1754
  orderViews(ids: string[]): void;
1753
1755
  /**
1754
1756
  * #action
1757
+ * Requires the view type's state model to be loaded; for a lazily
1758
+ * registered type use `launchView`.
1755
1759
  */
1756
- addView(typeName: string, initialState?: any): any;
1760
+ addView<N extends string>(typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): any;
1757
1761
  /**
1758
1762
  * #action
1759
1763
  * swap `view` for a new view of `typeName`, in the slot it occupied.
@@ -1783,7 +1787,7 @@ export declare function MultipleViewsSessionMixin(pluginManager: PluginManager):
1783
1787
  * its panel, and the launch that offers a replace is a click on that
1784
1788
  * view's own menu.
1785
1789
  */
1786
- replaceView(view: IBaseViewModel, typeName: string, initialState?: any): any;
1790
+ replaceView<N extends string>(view: IBaseViewModel, typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): any;
1787
1791
  /**
1788
1792
  * #action
1789
1793
  */
@@ -1845,6 +1849,16 @@ export declare function MultipleViewsSessionMixin(pluginManager: PluginManager):
1845
1849
  */
1846
1850
  resetUseWorkspaces(): void;
1847
1851
  afterAttach(): void;
1852
+ } & {
1853
+ /**
1854
+ * #action
1855
+ * `addView` for view types whose state model may be lazily loaded. The
1856
+ * view is not in `session.views` until the promise resolves.
1857
+ * `getViewType` first so an unknown type name fails by that name; the
1858
+ * snapshot walk skips unregistered names and covers the displays of the
1859
+ * tracks a launch opens with.
1860
+ */
1861
+ launchView<N extends string>(typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): Promise<any>;
1848
1862
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree").ModelSnapshotType<Omit<Omit<Omit<{
1849
1863
  id: import("@jbrowse/mobx-state-tree").IOptionalIType<import("@jbrowse/mobx-state-tree").ISimpleType<string>, [undefined]>;
1850
1864
  name: import("@jbrowse/mobx-state-tree").ISimpleType<string>;
@@ -1,7 +1,6 @@
1
- import { applyOrderWithin, localStorageGetBoolean, localStorageSetBoolean, reorderWithin, } from '@jbrowse/core/util';
1
+ import { applyOrderWithin, localStorageGetBoolean, localStorageSetBoolean, reorderWithin, scheduleDetachedDestroy, } from '@jbrowse/core/util';
2
2
  import { addDisposer, applyPatch, cast, detach, getMembers, getParent, hasParent, isReferenceType, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
3
3
  import { autorun } from 'mobx';
4
- import { scheduleDetachedDestroy } from '../scheduleDetachedDestroy.js';
5
4
  import { BaseSessionModel, isBaseSession } from './BaseSession.js';
6
5
  import { DrawerWidgetSessionMixin } from './DrawerWidgets.js';
7
6
  // Whether `node` IS `view` or lives somewhere under it. A view can CONTAIN
@@ -54,10 +53,24 @@ function dropRefsInto(holder, view) {
54
53
  }
55
54
  return found;
56
55
  }
56
+ // A session snapshot's `views`, or undefined for anything that is not one
57
+ function viewsOfSessionSnapshot(snapshot) {
58
+ const views = typeof snapshot === 'object' && snapshot !== null && 'views' in snapshot
59
+ ? snapshot.views
60
+ : undefined;
61
+ return Array.isArray(views) ? views : undefined;
62
+ }
57
63
  /**
58
64
  * #stateModel MultipleViewsSessionMixin
59
65
  */
60
66
  export function MultipleViewsSessionMixin(pluginManager) {
67
+ // an unknown type name passes through to fail at the union as it always has
68
+ function assertViewStateModelLoaded(typeName) {
69
+ if (pluginManager.getElementTypeRecord('view').has(typeName) &&
70
+ !pluginManager.getViewType(typeName).isStateModelLoaded) {
71
+ throw new Error(`state model for view type ${typeName} is not loaded yet — use launchView(), or await getViewType('${typeName}').loadStateModel() first`);
72
+ }
73
+ }
61
74
  return types
62
75
  .compose(BaseSessionModel(pluginManager), DrawerWidgetSessionMixin(pluginManager), types.model({
63
76
  /**
@@ -208,8 +221,11 @@ export function MultipleViewsSessionMixin(pluginManager) {
208
221
  },
209
222
  /**
210
223
  * #action
224
+ * Requires the view type's state model to be loaded; for a lazily
225
+ * registered type use `launchView`.
211
226
  */
212
- addView(typeName, initialState = {}) {
227
+ addView(typeName, initialState) {
228
+ assertViewStateModelLoaded(typeName);
213
229
  const length = self.views.push({
214
230
  ...initialState,
215
231
  type: typeName,
@@ -245,7 +261,8 @@ export function MultipleViewsSessionMixin(pluginManager) {
245
261
  * its panel, and the launch that offers a replace is a click on that
246
262
  * view's own menu.
247
263
  */
248
- replaceView(view, typeName, initialState = {}) {
264
+ replaceView(view, typeName, initialState) {
265
+ assertViewStateModelLoaded(typeName);
249
266
  // read before the removal, which is what makes both stale
250
267
  const idx = self.views.indexOf(view);
251
268
  // `idx` first, so a view already gone from the session short-circuits
@@ -305,7 +322,13 @@ export function MultipleViewsSessionMixin(pluginManager) {
305
322
  * lands, which is why ADR-069's rule stops there.
306
323
  */
307
324
  takeOutViewsMissingFrom(snapshot) {
308
- const incoming = snapshot?.views ?? [];
325
+ const incoming = viewsOfSessionSnapshot(snapshot);
326
+ // an argument that is not a session snapshot used to read as "keep
327
+ // none" and close every view: an agent introspecting the actions
328
+ // called this with a tab id and a keep-list and lost its session
329
+ if (!incoming) {
330
+ throw new Error('takeOutViewsMissingFrom takes a session snapshot (it is what undo and redo call to close the views a snapshot no longer has); to close one view, session.removeView(view)');
331
+ }
309
332
  const kept = new Map(incoming.map(v => [v.id, v.type]));
310
333
  // a copy, because `takeOut` splices the array it is iterating
311
334
  for (const view of [...self.views]) {
@@ -359,6 +382,23 @@ export function MultipleViewsSessionMixin(pluginManager) {
359
382
  },
360
383
  };
361
384
  })
385
+ .actions(self => ({
386
+ /**
387
+ * #action
388
+ * `addView` for view types whose state model may be lazily loaded. The
389
+ * view is not in `session.views` until the promise resolves.
390
+ * `getViewType` first so an unknown type name fails by that name; the
391
+ * snapshot walk skips unregistered names and covers the displays of the
392
+ * tracks a launch opens with.
393
+ */
394
+ async launchView(typeName, initialState) {
395
+ await pluginManager.getViewType(typeName).loadStateModel();
396
+ await pluginManager.preloadSessionTypes({
397
+ views: [{ type: typeName, ...initialState }],
398
+ });
399
+ return self.addView(typeName, initialState);
400
+ },
401
+ }))
362
402
  .postProcessSnapshot(snap => {
363
403
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
364
404
  if (!snap) {
@@ -1641,7 +1641,6 @@ export declare function PreferencesSessionMixin(pluginManager: PluginManager): i
1641
1641
  setPreferenceOverride(key: string, value: unknown): void;
1642
1642
  clearPreferenceOverrides(): void;
1643
1643
  clearPreferenceOverride(key: string): void;
1644
- resetPreferenceChange(path: string[]): void;
1645
1644
  setScrollZoom(flag: boolean): void;
1646
1645
  setDisplayTypeDefault(displayType: string, slot: string, value: unknown): void;
1647
1646
  setName(str: string): void;
@@ -1662,4 +1661,6 @@ export declare function PreferencesSessionMixin(pluginManager: PluginManager): i
1662
1661
  removeSnackbarMessage(message: string): void;
1663
1662
  } & {
1664
1663
  afterAttach(): void;
1664
+ } & {
1665
+ afterAttach(): void;
1665
1666
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
@@ -3,6 +3,7 @@ import { freezeDeep } from '@jbrowse/core/util/freezeDeep';
3
3
  import { addDisposer, types } from '@jbrowse/mobx-state-tree';
4
4
  import { autorun } from 'mobx';
5
5
  import { BaseSessionModel } from './BaseSession.js';
6
+ import { applyDeveloperMode } from './developerMode.js';
6
7
  const PREFS_KEY = 'jbrowsePreferences';
7
8
  function loadStoredPreferences() {
8
9
  const stored = localStorageGetItem(PREFS_KEY);
@@ -43,6 +44,10 @@ export function PreferencesSessionMixin(pluginManager) {
43
44
  // main-thread update would leave the app formatted two ways at once.
44
45
  // The dialog says a reload is needed; this is where the reload lands.
45
46
  setNumberGrouping(self.numberGrouping);
47
+ // and the same second pass for the developer-notice channel, whose
48
+ // admin default BaseSession already read: a user's stored override is
49
+ // only loaded a line above this
50
+ applyDeveloperMode(self);
46
51
  addDisposer(self, autorun(function persistPreferences() {
47
52
  // observable.map doesn't JSON-serialize to an object on its own;
48
53
  // fromEntries also reads every key so the autorun re-persists on
@@ -1670,7 +1670,6 @@ export declare function SessionTracksManagerSessionMixin(pluginManager: PluginMa
1670
1670
  setPreferenceOverride(key: string, value: unknown): void;
1671
1671
  clearPreferenceOverrides(): void;
1672
1672
  clearPreferenceOverride(key: string): void;
1673
- resetPreferenceChange(path: string[]): void;
1674
1673
  setScrollZoom(flag: boolean): void;
1675
1674
  setDisplayTypeDefault(displayType: string, slot: string, value: unknown): void;
1676
1675
  setName(str: string): void;
@@ -1689,6 +1688,8 @@ export declare function SessionTracksManagerSessionMixin(pluginManager: PluginMa
1689
1688
  pushSnackbarMessage(message: string, level?: import("@jbrowse/core/util").NotificationLevel, actions?: import("@jbrowse/core/util").SnackAction[]): void;
1690
1689
  popSnackbarMessage(): import("@jbrowse/core/ui/SnackbarModel").SnackbarMessage | undefined;
1691
1690
  removeSnackbarMessage(message: string): void;
1691
+ } & {
1692
+ afterAttach(): void;
1692
1693
  } & {
1693
1694
  getReferringMultiple(trackIds: string[]): Map<string, import("./ReferenceManagement.ts").ReferringNode[]>;
1694
1695
  } & {
@@ -1700,6 +1701,8 @@ export declare function SessionTracksManagerSessionMixin(pluginManager: PluginMa
1700
1701
  } & {
1701
1702
  getTrackById(id: string): AnyConfigurationModel | undefined;
1702
1703
  getTracksById(): Record<string, AnyConfigurationModel>;
1704
+ } & {
1705
+ afterAttach(): void;
1703
1706
  } & {
1704
1707
  addSessionTrackConf(trackConf: AnyConfiguration): any;
1705
1708
  publishTrackConf(trackConf: AnyConfiguration): any;
@@ -4,6 +4,7 @@ import { expandLooseTrackConfig } from '@jbrowse/core/util/tracks';
4
4
  import { applySnapshot, getSnapshot, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
5
5
  import { compareStructural } from 'mobx';
6
6
  import { TracksManagerSessionMixin } from './Tracks.js';
7
+ import { assertNotReaddedDifferently } from './readdedTrackConf.js';
7
8
  import { assertTrackConfOutlivesItsAssemblies } from './temporaryAssemblyTracks.js';
8
9
  // jbrowse.tracks holds frozen plain objects (app-core, web/desktop) or MST
9
10
  // config nodes (product-core, embedded react views); the delta math reads both
@@ -342,6 +343,13 @@ export function SessionTracksManagerSessionMixin(pluginManager) {
342
343
  // and dropping its trackConfigDeltas override semantics.
343
344
  const existing = self.getTrackById(trackId);
344
345
  if (existing) {
346
+ if (self.sessionTracks.includes(existing)) {
347
+ assertNotReaddedDifferently(pluginManager, existing, {
348
+ ...trackConf,
349
+ trackId,
350
+ type,
351
+ });
352
+ }
345
353
  return existing;
346
354
  }
347
355
  // sessionTracks is a typed MST array (unlike the frozen
@@ -44,7 +44,7 @@ export function trackActionItems({ session, config, view, canEdit, isSessionOver
44
44
  // visitor is served, one per click since the copy's stamped trackId defeats
45
45
  // the dedupe, and a non-admin's lands as the dead `sessionTracks` entry
46
46
  // ADR-084 removed the sweep for. Neither is worth offering, so the item says
47
- // so instead of producing garbage; the dev-only contract check on the adders
47
+ // so instead of producing garbage; the contract check on the adders
48
48
  // is what found this.
49
49
  //
50
50
  // Greying a row tells nobody anything, and this reason is one nobody guesses:
@@ -86,7 +86,7 @@ export function trackActionItems({ session, config, view, canEdit, isSessionOver
86
86
  onClick: () => {
87
87
  const snap = makeCopy();
88
88
  if (session.publishTrackConf(snap)) {
89
- view.showTrack(snap.trackId);
89
+ void view.launchTrack(snap.trackId);
90
90
  }
91
91
  },
92
92
  },
@@ -1638,7 +1638,6 @@ export declare function TracksManagerSessionMixin(pluginManager: PluginManager):
1638
1638
  setPreferenceOverride(key: string, value: unknown): void;
1639
1639
  clearPreferenceOverrides(): void;
1640
1640
  clearPreferenceOverride(key: string): void;
1641
- resetPreferenceChange(path: string[]): void;
1642
1641
  setScrollZoom(flag: boolean): void;
1643
1642
  setDisplayTypeDefault(displayType: string, slot: string, value: unknown): void;
1644
1643
  setName(str: string): void;
@@ -1657,6 +1656,8 @@ export declare function TracksManagerSessionMixin(pluginManager: PluginManager):
1657
1656
  pushSnackbarMessage(message: string, level?: import("@jbrowse/core/util").NotificationLevel, actions?: import("@jbrowse/core/util").SnackAction[]): void;
1658
1657
  popSnackbarMessage(): import("@jbrowse/core/ui/SnackbarModel").SnackbarMessage | undefined;
1659
1658
  removeSnackbarMessage(message: string): void;
1659
+ } & {
1660
+ afterAttach(): void;
1660
1661
  } & {
1661
1662
  getReferringMultiple(trackIds: string[]): Map<string, import("./ReferenceManagement.ts").ReferringNode[]>;
1662
1663
  } & {
@@ -1689,6 +1690,8 @@ export declare function TracksManagerSessionMixin(pluginManager: PluginManager):
1689
1690
  * @deprecated
1690
1691
  */
1691
1692
  getTracksById(): Record<string, AnyConfigurationModel>;
1693
+ } & {
1694
+ afterAttach(): void;
1692
1695
  } & {
1693
1696
  /**
1694
1697
  * #action
@@ -1734,10 +1737,6 @@ export declare function TracksManagerSessionMixin(pluginManager: PluginManager):
1734
1737
  * session, which is the destination every such caller wanted: a track a
1735
1738
  * plugin stands up for one user is not a catalog entry. Nothing in tree
1736
1739
  * may call it — `no-restricted-syntax` says so.
1737
- *
1738
- * Spelled out rather than delegating through `this`, for the reason
1739
- * `SessionTracks.addToSession` is a plain closure: an action whose return
1740
- * type is inferred from a sibling's makes the pair circular.
1741
1740
  */
1742
1741
  addTrackConf(trackConf: AnyConfiguration): any;
1743
1742
  /**