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

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 (133) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +629 -304
  2. package/esm/RootModel/BaseRootModel.js +138 -14
  3. package/esm/RootModel/HierarchicalConfig.d.ts +57 -21
  4. package/esm/RootModel/HierarchicalConfig.js +36 -0
  5. package/esm/RootModel/InternetAccounts.d.ts +18 -1
  6. package/esm/RootModel/InternetAccounts.js +120 -22
  7. package/esm/RootModel/PreferencesConfig.d.ts +53 -0
  8. package/esm/RootModel/PreferencesConfig.js +61 -0
  9. package/esm/RootModel/createConfigModel.d.ts +232 -0
  10. package/esm/RootModel/createConfigModel.js +56 -0
  11. package/esm/RootModel/index.d.ts +10 -5
  12. package/esm/RootModel/index.js +10 -5
  13. package/esm/RootModel/menuItems.d.ts +24 -0
  14. package/esm/RootModel/menuItems.js +143 -0
  15. package/esm/Session/BaseSession.d.ts +310 -11
  16. package/esm/Session/BaseSession.js +448 -14
  17. package/esm/Session/Connections.d.ts +114 -8
  18. package/esm/Session/Connections.js +195 -74
  19. package/esm/Session/DrawerWidgets.d.ts +115 -12
  20. package/esm/Session/DrawerWidgets.js +161 -42
  21. package/esm/Session/MultipleViews.d.ts +1266 -1128
  22. package/esm/Session/MultipleViews.js +357 -66
  23. package/esm/Session/Preferences.d.ts +1665 -0
  24. package/esm/Session/Preferences.js +54 -0
  25. package/esm/Session/ReferenceManagement.d.ts +28 -3
  26. package/esm/Session/ReferenceManagement.js +56 -41
  27. package/esm/Session/SessionTracks.d.ts +1275 -1138
  28. package/esm/Session/SessionTracks.js +532 -35
  29. package/esm/Session/Themes.d.ts +87 -2
  30. package/esm/Session/Themes.js +156 -23
  31. package/esm/Session/TrackMenu.d.ts +112 -0
  32. package/esm/Session/TrackMenu.js +200 -0
  33. package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
  34. package/esm/Session/TrackMenuSessionMixin.js +41 -0
  35. package/esm/Session/Tracks.d.ts +1169 -1117
  36. package/esm/Session/Tracks.js +160 -20
  37. package/esm/Session/assertSessionModel.d.ts +21 -0
  38. package/esm/Session/assertSessionModel.js +1 -0
  39. package/esm/Session/finalizeSession.d.ts +16 -0
  40. package/esm/Session/finalizeSession.js +29 -0
  41. package/esm/Session/index.d.ts +22 -10
  42. package/esm/Session/index.js +13 -10
  43. package/esm/Session/shareableSnapshot.d.ts +77 -0
  44. package/esm/Session/shareableSnapshot.js +196 -0
  45. package/esm/Session/temporaryAssemblyTracks.d.ts +23 -0
  46. package/esm/Session/temporaryAssemblyTracks.js +38 -0
  47. package/esm/assertExtends.d.ts +55 -0
  48. package/esm/assertExtends.js +1 -0
  49. package/esm/controllerTracks.d.ts +56 -0
  50. package/esm/controllerTracks.js +77 -0
  51. package/esm/destroyViewState.d.ts +21 -0
  52. package/esm/destroyViewState.js +29 -0
  53. package/esm/index.d.ts +31 -4
  54. package/esm/index.js +31 -4
  55. package/esm/loadPlugins.d.ts +33 -0
  56. package/esm/loadPlugins.js +38 -0
  57. package/esm/localFiles.d.ts +74 -0
  58. package/esm/localFiles.js +156 -0
  59. package/esm/observeSession.d.ts +56 -0
  60. package/esm/observeSession.js +91 -0
  61. package/esm/pluginInput.d.ts +32 -0
  62. package/esm/pluginInput.js +19 -0
  63. package/esm/pruneUnbuildableNodes.d.ts +109 -0
  64. package/esm/pruneUnbuildableNodes.js +568 -0
  65. package/esm/resolveAssemblies.d.ts +44 -0
  66. package/esm/resolveAssemblies.js +55 -0
  67. package/esm/rpcWorker.d.ts +26 -0
  68. package/esm/rpcWorker.js +96 -15
  69. package/esm/scheduleDetachedDestroy.d.ts +18 -0
  70. package/esm/scheduleDetachedDestroy.js +24 -0
  71. package/esm/sessionMigrations/index.d.ts +53 -0
  72. package/esm/sessionMigrations/index.js +432 -0
  73. package/esm/sessionUrl.d.ts +41 -0
  74. package/esm/sessionUrl.js +53 -0
  75. package/esm/sessionUtils.d.ts +69 -0
  76. package/esm/sessionUtils.js +613 -0
  77. package/esm/siblingCast.d.ts +14 -0
  78. package/esm/siblingCast.js +16 -0
  79. package/esm/snapshotUtils.d.ts +8 -0
  80. package/esm/snapshotUtils.js +12 -0
  81. package/esm/ui/AboutDialog.d.ts +3 -6
  82. package/esm/ui/AboutDialog.js +4 -4
  83. package/esm/ui/AboutDialogContents.d.ts +2 -6
  84. package/esm/ui/AboutDialogContents.js +34 -13
  85. package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
  86. package/esm/ui/AssemblyInfoPanel.js +69 -0
  87. package/esm/ui/DisplayDefaultsSection.d.ts +36 -0
  88. package/esm/ui/DisplayDefaultsSection.js +65 -0
  89. package/esm/ui/Drawer.d.ts +21 -0
  90. package/esm/ui/Drawer.js +60 -0
  91. package/esm/ui/DrawerControls.d.ts +5 -0
  92. package/esm/ui/DrawerControls.js +28 -0
  93. package/esm/ui/DrawerHeader.d.ts +6 -0
  94. package/esm/ui/DrawerHeader.js +36 -0
  95. package/esm/ui/DrawerWidget.d.ts +5 -0
  96. package/esm/ui/DrawerWidget.js +12 -0
  97. package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
  98. package/esm/ui/DrawerWidgetSelector.js +40 -0
  99. package/esm/ui/FileInfoPanel.d.ts +3 -6
  100. package/esm/ui/FileInfoPanel.js +35 -26
  101. package/esm/ui/HeaderButtons.d.ts +7 -1
  102. package/esm/ui/HeaderButtons.js +23 -18
  103. package/esm/ui/ModalWidget.d.ts +12 -0
  104. package/esm/ui/ModalWidget.js +33 -0
  105. package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
  106. package/esm/ui/ModalWidgetAppBar.js +9 -0
  107. package/esm/ui/PreferencesDialog.d.ts +56 -0
  108. package/esm/ui/PreferencesDialog.js +145 -0
  109. package/esm/ui/PreferencesResetDialog.d.ts +8 -0
  110. package/esm/ui/PreferencesResetDialog.js +19 -0
  111. package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
  112. package/esm/ui/RefNameAliasesDialog.js +65 -0
  113. package/esm/ui/RefNameInfoDialog.d.ts +3 -6
  114. package/esm/ui/RefNameInfoDialog.js +57 -63
  115. package/esm/ui/WidgetBody.d.ts +18 -0
  116. package/esm/ui/WidgetBody.js +22 -0
  117. package/esm/ui/WidgetHeading.d.ts +7 -0
  118. package/esm/ui/WidgetHeading.js +7 -0
  119. package/esm/ui/drawerLayout.d.ts +18 -0
  120. package/esm/ui/drawerLayout.js +22 -0
  121. package/esm/ui/index.d.ts +13 -1
  122. package/esm/ui/index.js +12 -1
  123. package/esm/ui/util.d.ts +37 -4
  124. package/esm/ui/util.js +35 -38
  125. package/esm/useEngineLifecycle.d.ts +13 -0
  126. package/esm/useEngineLifecycle.js +22 -0
  127. package/package.json +11 -15
  128. package/esm/RootModel/FormatAbout.d.ts +0 -12
  129. package/esm/RootModel/FormatAbout.js +0 -15
  130. package/esm/RootModel/FormatDetails.d.ts +0 -24
  131. package/esm/RootModel/FormatDetails.js +0 -27
  132. package/esm/Session/DialogQueue.d.ts +0 -17
  133. package/esm/Session/DialogQueue.js +0 -31
@@ -1,47 +1,171 @@
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 { cast, getSnapshot, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
5
- export function BaseRootModelFactory({ pluginManager, jbrowseModelType, sessionModelType, assemblyConfigSchema, }) {
4
+ import { cast, detach, getType, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
5
+ import { describeUnbuildableNodes, pruneUnbuildableNodes, } from '../pruneUnbuildableNodes.js';
6
+ import { scheduleDetachedDestroy } from '../scheduleDetachedDestroy.js';
7
+ import { migrateSessionSnapshot } from '../sessionMigrations/index.js';
8
+ import { filterSessionInPlace } from '../sessionUtils.js';
9
+ // `session` is a `types.maybe(sessionModelType)` where `sessionModelType` is the
10
+ // erased `IAnyType` (product-core can't name the concrete per-product session
11
+ // type without a root↔session cycle), so `self.session` degrades to `any`. Every
12
+ // concrete session composes BaseSessionModel + SnackbarModel, so asserting the
13
+ // shared `BaseSession` contract restores real checking on the `setName`/`notify`
14
+ // members the base root reaches for — without a hand-maintained shadow.
15
+ /**
16
+ * #stateModel BaseRootModel
17
+ * #category root
18
+ * factory function for the Base-level root model shared by all products
19
+ */
20
+ export function BaseRootModelFactory({ pluginManager, jbrowseModelType, sessionModelType, assemblyConfigSchema, rpcManagerOptions, }) {
6
21
  return types
7
22
  .model('BaseRootModel', {
23
+ /**
24
+ * #property
25
+ * `jbrowse` is a mapping of the config.json into the in-memory state
26
+ * tree
27
+ */
8
28
  jbrowse: jbrowseModelType,
29
+ /**
30
+ * #property
31
+ * `session` encompasses the currently active state of the app, including
32
+ * views open, tracks open in those views, etc.
33
+ */
9
34
  session: types.maybe(sessionModelType),
10
- sessionPath: types.optional(types.string, ''),
35
+ /**
36
+ * #property
37
+ */
38
+ sessionPath: types.stripDefault(types.string, ''),
39
+ /**
40
+ * #property
41
+ */
11
42
  assemblyManager: types.optional(assemblyManagerFactory(assemblyConfigSchema, pluginManager), {}),
12
43
  })
13
44
  .volatile(self => ({
14
- rpcManager: new RpcManager(pluginManager, self.jbrowse.configuration.rpc, {
15
- MainThreadRpcDriver: {},
16
- }),
45
+ /**
46
+ * #volatile
47
+ */
48
+ rpcManager: new RpcManager(pluginManager, self.jbrowse.configuration.rpc, rpcManagerOptions),
49
+ /**
50
+ * #volatile
51
+ */
17
52
  adminMode: false,
53
+ /**
54
+ * #volatile
55
+ */
56
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
18
57
  error: undefined,
58
+ /**
59
+ * #volatile
60
+ */
19
61
  textSearchManager: new TextSearchManager(pluginManager),
62
+ /**
63
+ * #volatile
64
+ */
20
65
  pluginManager,
21
66
  }))
22
67
  .actions(self => ({
68
+ /**
69
+ * #action
70
+ */
23
71
  setError(error) {
24
72
  self.error = error;
25
73
  },
74
+ /**
75
+ * #action
76
+ * Sets the active session. Remaps any legacy display type names
77
+ * (e.g. LinearPileupDisplay → LinearAlignmentsDisplay), drops nodes whose
78
+ * pluggable type this build has no plugin for (see
79
+ * `pruneUnbuildableNodes`), then walks the resulting MST tree to drop open
80
+ * tracks whose config can't hydrate so shared sessions still load when
81
+ * referencing tracks that no longer exist. Both kinds of drop are surfaced
82
+ * to the user via a snackbar. If filtering throws, the previous session is
83
+ * restored.
84
+ */
26
85
  setSession(sessionSnapshot) {
27
- self.session = cast(sessionSnapshot);
86
+ const oldSession = self.session;
87
+ const unbuildable = [];
88
+ let migrated = sessionSnapshot;
89
+ if (sessionSnapshot && typeof sessionSnapshot === 'object') {
90
+ // before the cast, because a type this build has no plugin for is a
91
+ // union failure the try below never gets to see
92
+ const pruned = pruneUnbuildableNodes(migrateSessionSnapshot(sessionSnapshot), pluginManager, sessionModelType);
93
+ migrated = pruned.snapshot;
94
+ unbuildable.push(...pruned.dropped);
95
+ }
96
+ // Detach first: assigning over it would destroy it inside this action,
97
+ // and MobX runs the action's pending reactions afterwards, against the
98
+ // nodes it just killed. ADR-069.
99
+ if (oldSession) {
100
+ detach(oldSession);
101
+ }
102
+ // The assignment is inside the try because it typechecks: a registered
103
+ // type carrying a malformed prop is a throw `pruneUnbuildableNodes`
104
+ // does not and cannot pre-empt, and the detach above has already
105
+ // emptied `self.session`. MST does not roll an action back, so without
106
+ // this the root would be left with no session at all and the old tree
107
+ // detached-and-alive forever.
108
+ try {
109
+ self.session = cast(migrated);
110
+ if (self.session) {
111
+ const unbuildableMessage = describeUnbuildableNodes(unbuildable);
112
+ if (unbuildableMessage) {
113
+ ;
114
+ self.session.notify(unbuildableMessage, 'warning');
115
+ }
116
+ const dropped = filterSessionInPlace(self.session, getType(self.session));
117
+ if (dropped.length > 0) {
118
+ const names = dropped
119
+ .map(d => d.configuration ?? d.type ?? 'unknown')
120
+ .join(', ');
121
+ const plural = dropped.length > 1;
122
+ self.session.notify(`Removed ${dropped.length} track${plural ? 's' : ''} that could not be loaded: ${names}`, 'warning');
123
+ }
124
+ }
125
+ }
126
+ catch (error) {
127
+ // put it back, and do not schedule the destroy below — this is the
128
+ // one path where the old session goes on being the live one
129
+ self.session = oldSession;
130
+ throw error;
131
+ }
132
+ // and it does still get destroyed, once the reaction flush has
133
+ // unwound. `beforeDestroy` is a plugin-facing contract and leaving a
134
+ // detached session alive forever would skip it. ADR-069.
135
+ if (oldSession) {
136
+ scheduleDetachedDestroy(oldSession);
137
+ }
28
138
  },
139
+ /**
140
+ * #action
141
+ */
29
142
  setDefaultSession() {
30
- this.setSession(self.jbrowse.defaultSession);
143
+ const { defaultSession } = self.jbrowse;
144
+ this.setSession({
145
+ ...defaultSession,
146
+ // timestamp the name so repeated "new session" names don't collide
147
+ name: `${defaultSession.name || 'New session'} ${new Date().toLocaleString()}`,
148
+ });
31
149
  },
150
+ /**
151
+ * #action
152
+ */
32
153
  setSessionPath(path) {
33
154
  self.sessionPath = path;
34
155
  },
156
+ /**
157
+ * #action
158
+ */
35
159
  renameCurrentSession(newName) {
36
- if (self.session) {
37
- this.setSession({
38
- ...getSnapshot(self.session),
39
- name: newName,
40
- });
41
- }
160
+ // Every concrete session model is composed from BaseSessionModel, which
161
+ // provides setName — avoid a full setSession rebuild here since the
162
+ // only field changing is `name`.
163
+ ;
164
+ self.session?.setName(newName);
42
165
  },
43
166
  }));
44
167
  }
168
+ /** Type guard for checking if something is a JB root model */
45
169
  export function isRootModel(thing) {
46
170
  return (isStateTreeNode(thing) &&
47
171
  'session' in thing &&
@@ -1,31 +1,67 @@
1
+ /**
2
+ * #config HierarchicalConfigSchema
3
+ * #category root
4
+ * generally exists on the config.json or root config as configuration.hierarchical
5
+ */
1
6
  export declare function HierarchicalConfigSchemaFactory(): import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
2
- sort: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
3
- trackNames: {
4
- type: string;
5
- defaultValue: boolean;
7
+ readonly sort: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
8
+ /**
9
+ * #slot configuration.hierarchical.sort.trackNames
10
+ * start the track selector with tracks sorted alphabetically inside each
11
+ * category rather than in config order. Only the initial state — the
12
+ * selector's own "Sort track names" toggle overrides it from then on
13
+ */
14
+ readonly trackNames: {
15
+ readonly type: "boolean";
16
+ readonly defaultValue: false;
6
17
  };
7
- categories: {
8
- type: string;
9
- defaultValue: boolean;
18
+ /**
19
+ * #slot configuration.hierarchical.sort.categories
20
+ * start the track selector with categories sorted alphabetically rather
21
+ * than in config order. Only the initial state — the selector's own
22
+ * "Sort categories" toggle overrides it from then on
23
+ */
24
+ readonly categories: {
25
+ readonly type: "boolean";
26
+ readonly defaultValue: false;
10
27
  };
11
28
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
12
- defaultFolderCategories: {
13
- type: string;
14
- description: string;
15
- defaultValue: never[];
29
+ /**
30
+ * #slot configuration.hierarchical.defaultFolderCategories
31
+ */
32
+ readonly defaultFolderCategories: {
33
+ readonly type: "stringArray";
34
+ readonly description: "list of category names to display as folders by default";
35
+ readonly defaultValue: readonly [];
16
36
  };
17
- defaultCollapsed: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
18
- categoryNames: {
19
- type: string;
20
- defaultValue: never[];
37
+ readonly defaultCollapsed: import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
38
+ /**
39
+ * #slot configuration.hierarchical.defaultCollapsed.categoryNames
40
+ * named top-level categories that start collapsed, e.g.
41
+ * `['ENCODE', 'RNA-seq']`. Once a user expands or collapses anything, the
42
+ * selector remembers their state instead
43
+ */
44
+ readonly categoryNames: {
45
+ readonly type: "stringArray";
46
+ readonly defaultValue: readonly [];
21
47
  };
22
- topLevelCategories: {
23
- type: string;
24
- defaultValue: boolean;
48
+ /**
49
+ * #slot configuration.hierarchical.defaultCollapsed.topLevelCategories
50
+ * start every top-level category collapsed, so a config with many
51
+ * categories opens as a short list rather than a wall of tracks
52
+ */
53
+ readonly topLevelCategories: {
54
+ readonly type: "boolean";
55
+ readonly defaultValue: false;
25
56
  };
26
- subCategories: {
27
- type: string;
28
- defaultValue: boolean;
57
+ /**
58
+ * #slot configuration.hierarchical.defaultCollapsed.subCategories
59
+ * start every nested category collapsed while leaving the top level
60
+ * open, for deeply categorized track sets
61
+ */
62
+ readonly subCategories: {
63
+ readonly type: "boolean";
64
+ readonly defaultValue: false;
29
65
  };
30
66
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
31
67
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;
@@ -1,30 +1,66 @@
1
1
  import { ConfigurationSchema } from '@jbrowse/core/configuration';
2
+ /**
3
+ * #config HierarchicalConfigSchema
4
+ * #category root
5
+ * generally exists on the config.json or root config as configuration.hierarchical
6
+ */
2
7
  export function HierarchicalConfigSchemaFactory() {
3
8
  return ConfigurationSchema('hierarchical', {
4
9
  sort: ConfigurationSchema('hierarchicalSort', {
10
+ /**
11
+ * #slot configuration.hierarchical.sort.trackNames
12
+ * start the track selector with tracks sorted alphabetically inside each
13
+ * category rather than in config order. Only the initial state — the
14
+ * selector's own "Sort track names" toggle overrides it from then on
15
+ */
5
16
  trackNames: {
6
17
  type: 'boolean',
7
18
  defaultValue: false,
8
19
  },
20
+ /**
21
+ * #slot configuration.hierarchical.sort.categories
22
+ * start the track selector with categories sorted alphabetically rather
23
+ * than in config order. Only the initial state — the selector's own
24
+ * "Sort categories" toggle overrides it from then on
25
+ */
9
26
  categories: {
10
27
  type: 'boolean',
11
28
  defaultValue: false,
12
29
  },
13
30
  }),
31
+ /**
32
+ * #slot configuration.hierarchical.defaultFolderCategories
33
+ */
14
34
  defaultFolderCategories: {
15
35
  type: 'stringArray',
16
36
  description: 'list of category names to display as folders by default',
17
37
  defaultValue: [],
18
38
  },
19
39
  defaultCollapsed: ConfigurationSchema('defaultCollapsed', {
40
+ /**
41
+ * #slot configuration.hierarchical.defaultCollapsed.categoryNames
42
+ * named top-level categories that start collapsed, e.g.
43
+ * `['ENCODE', 'RNA-seq']`. Once a user expands or collapses anything, the
44
+ * selector remembers their state instead
45
+ */
20
46
  categoryNames: {
21
47
  type: 'stringArray',
22
48
  defaultValue: [],
23
49
  },
50
+ /**
51
+ * #slot configuration.hierarchical.defaultCollapsed.topLevelCategories
52
+ * start every top-level category collapsed, so a config with many
53
+ * categories opens as a short list rather than a wall of tracks
54
+ */
24
55
  topLevelCategories: {
25
56
  type: 'boolean',
26
57
  defaultValue: false,
27
58
  },
59
+ /**
60
+ * #slot configuration.hierarchical.defaultCollapsed.subCategories
61
+ * start every nested category collapsed while leaving the top level
62
+ * open, for deeply categorized track sets
63
+ */
28
64
  subCategories: {
29
65
  type: 'boolean',
30
66
  defaultValue: false,
@@ -2,11 +2,28 @@ import type PluginManager from '@jbrowse/core/PluginManager';
2
2
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
3
3
  import type { UriLocation } from '@jbrowse/core/util';
4
4
  import type { Instance } from '@jbrowse/mobx-state-tree';
5
+ /**
6
+ * #stateModel InternetAccountsMixin
7
+ * #category root
8
+ */
5
9
  export declare function InternetAccountsRootModelMixin(pluginManager: PluginManager): import("@jbrowse/mobx-state-tree").IModelType<{
10
+ /**
11
+ * #property
12
+ */
6
13
  internetAccounts: import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IAnyType>;
7
14
  }, {
8
- initializeInternetAccount(internetAccountConfig: AnyConfigurationModel, initialSnapshot?: {}): any;
15
+ /**
16
+ * #action
17
+ */
18
+ initializeInternetAccount(internetAccountConfig: AnyConfigurationModel, initialSnapshot?: object): any;
19
+ /**
20
+ * #action
21
+ */
9
22
  createEphemeralInternetAccount(internetAccountId: string, initialSnapshot: Record<string, unknown>, url: string): any;
23
+ } & {
24
+ /**
25
+ * #action
26
+ */
10
27
  findAppropriateInternetAccount(location: UriLocation): any;
11
28
  } & {
12
29
  afterCreate(): void;
@@ -1,74 +1,172 @@
1
1
  import { addDisposer, types } from '@jbrowse/mobx-state-tree';
2
2
  import { autorun } from 'mobx';
3
+ import { asRoot } from '../siblingCast.js';
4
+ /**
5
+ * The ephemeral account ids RpcManager mints are `<TypeName>-<rest>` — the
6
+ * leading segment names the account type and the remainder is its display name.
7
+ * Read in one place so the two callers cannot disagree about which is which.
8
+ */
9
+ function parseEphemeralId(internetAccountId) {
10
+ const [type = '', ...rest] = internetAccountId.split('-');
11
+ return { type, name: rest.join('-') };
12
+ }
13
+ /**
14
+ * Whether the fetch this location describes would go to the origin serving
15
+ * JBrowse itself.
16
+ *
17
+ * `domains` cannot name that host. A config shipped with relative data paths
18
+ * (`volvox_microarray.bw`) resolves against wherever the app is deployed, which
19
+ * whoever wrote the config does not know — so an account for it has nothing to
20
+ * put in `domains`, and naming the account on the location is the only way to
21
+ * reach it. Safe whatever `domains` says: a link can move a same-origin
22
+ * location nowhere except the server already serving the page and holding the
23
+ * session.
24
+ *
25
+ * Resolved the way the request will be, against `baseUri` when there is one, so
26
+ * a spec cannot pass a relative uri under an off-origin base and read as local.
27
+ */
28
+ function resolvesToAppOrigin(location) {
29
+ // typed as always present, and absent in node and in some embedded hosts
30
+ const here = globalThis.location;
31
+ // `null` is what an opaque origin (file:, data:) stringifies to, and two of
32
+ // them are not the same origin
33
+ if (!here?.origin || here.origin === 'null') {
34
+ return false;
35
+ }
36
+ try {
37
+ return (new URL(location.uri, location.baseUri ?? here.href).origin ===
38
+ here.origin);
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ // An account named by a location it is not scoped for reads as a dead track,
45
+ // so say which half to fix. Once per pair: the check runs on every RPC
46
+ // serialization, and a track issues one per block.
47
+ const warnedScopeMismatches = new Set();
48
+ function warnScopeMismatch(internetAccountId, uri) {
49
+ const key = `${internetAccountId}|${uri}`;
50
+ if (!warnedScopeMismatches.has(key)) {
51
+ warnedScopeMismatches.add(key);
52
+ console.warn(`Internet account "${internetAccountId}" is not used for ${uri}: the URL is outside its "domains". Add the host to that account's domains if this is your data.`);
53
+ }
54
+ }
55
+ /**
56
+ * #stateModel InternetAccountsMixin
57
+ * #category root
58
+ */
3
59
  export function InternetAccountsRootModelMixin(pluginManager) {
4
60
  return types
5
61
  .model({
62
+ /**
63
+ * #property
64
+ */
6
65
  internetAccounts: types.array(pluginManager.pluggableMstType('internet account', 'stateModel')),
7
66
  })
8
67
  .actions(self => ({
68
+ /**
69
+ * #action
70
+ */
9
71
  initializeInternetAccount(internetAccountConfig, initialSnapshot = {}) {
10
- const internetAccountType = pluginManager.getInternetAccountType(internetAccountConfig.type);
11
- if (!internetAccountType) {
12
- throw new Error(`unknown internet account type ${internetAccountConfig.type}`);
13
- }
14
- const length = self.internetAccounts.push({
72
+ self.internetAccounts.push({
15
73
  ...initialSnapshot,
16
74
  type: internetAccountConfig.type,
17
75
  configuration: internetAccountConfig,
18
76
  });
19
- return self.internetAccounts[length - 1];
77
+ return self.internetAccounts.at(-1);
20
78
  },
79
+ /**
80
+ * #action
81
+ */
21
82
  createEphemeralInternetAccount(internetAccountId, initialSnapshot, url) {
22
83
  let hostUri;
23
84
  try {
24
- hostUri = new URL(url).origin;
85
+ const urlObj = new URL(url);
86
+ const pathname = urlObj.pathname;
87
+ const lastSlash = pathname.lastIndexOf('/');
88
+ const dirPath = lastSlash !== -1 ? pathname.slice(0, lastSlash + 1) : '/';
89
+ hostUri = `${urlObj.origin}${dirPath}`;
25
90
  }
26
- catch (e) {
91
+ catch {
92
+ // ignore
27
93
  }
28
- const internetAccountSplit = internetAccountId.split('-');
94
+ const { type, name } = parseEphemeralId(internetAccountId);
29
95
  const configuration = {
30
- type: internetAccountSplit[0],
31
- internetAccountId: internetAccountId,
32
- name: internetAccountSplit.slice(1).join('-'),
96
+ type,
97
+ internetAccountId,
98
+ name,
33
99
  description: '',
34
100
  domains: hostUri ? [hostUri] : [],
35
101
  };
36
- const type = pluginManager.getInternetAccountType(configuration.type);
37
- const internetAccount = type.stateModel.create({
102
+ self.internetAccounts.push({
38
103
  ...initialSnapshot,
39
104
  type: configuration.type,
40
105
  configuration,
41
106
  });
42
- self.internetAccounts.push(internetAccount);
43
- return internetAccount;
107
+ return self.internetAccounts.at(-1);
44
108
  },
109
+ }))
110
+ .actions(self => ({
111
+ /**
112
+ * #action
113
+ */
45
114
  findAppropriateInternetAccount(location) {
115
+ // find the existing account selected from menu
46
116
  const selectedId = location.internetAccountId;
47
117
  if (selectedId) {
48
118
  const selectedAccount = self.internetAccounts.find(account => {
49
119
  return account.internetAccountId === selectedId;
50
120
  });
121
+ // The account still has to be scoped for the URL. Naming one used to
122
+ // be enough on its own, which made the field a way to redirect
123
+ // someone else's credential: jbrowse-web builds tracks out of
124
+ // `sessionTracks` in the URL, and a location there carrying
125
+ // `internetAccountId` sent that account's token to whatever host the
126
+ // link chose — first as validateToken's probe from the main thread,
127
+ // then as the worker's read. An OAuth account with a refresh token in
128
+ // localStorage needs no interaction at all to mint a fresh one for it.
51
129
  if (selectedAccount) {
52
- return selectedAccount;
130
+ if (selectedAccount.handlesLocation(location) ||
131
+ resolvesToAppOrigin(location)) {
132
+ return selectedAccount;
133
+ }
134
+ warnScopeMismatch(selectedId, location.uri);
53
135
  }
54
136
  }
137
+ // if no existing account or not found, try to find working account
55
138
  for (const account of self.internetAccounts) {
56
139
  const handleResult = account.handlesLocation(location);
57
140
  if (handleResult) {
58
141
  return account;
59
142
  }
60
143
  }
61
- return selectedId
62
- ? this.createEphemeralInternetAccount(selectedId, {}, location.uri)
63
- : null;
144
+ // if still no existing account, create ephemeral config to use — but
145
+ // only for an id whose leading segment names an account type this
146
+ // session actually has. Anything else is a location pointing at an
147
+ // account that is simply gone (a track from a shared session whose
148
+ // config defined one, a config it was removed from, a host that never
149
+ // loaded the authentication plugin), and pushing an unknown type into
150
+ // the array threw a bare MST union error out of whatever was opening
151
+ // the file. No account instead lets the read go out unauthenticated and
152
+ // report the 401 it gets, which is the failure the user can act on.
153
+ if (selectedId &&
154
+ pluginManager
155
+ .getElementTypesInGroup('internet account')
156
+ .some(t => t.name === parseEphemeralId(selectedId).type)) {
157
+ return self.createEphemeralInternetAccount(selectedId, {}, location.uri);
158
+ }
159
+ return null;
64
160
  },
65
161
  }))
66
162
  .actions(self => ({
67
163
  afterCreate() {
68
164
  addDisposer(self, autorun(function internetAccountsAutorun() {
69
- const { jbrowse } = self;
165
+ const { jbrowse } = asRoot(self);
70
166
  for (const internetAccount of jbrowse.internetAccounts) {
71
- self.initializeInternetAccount(internetAccount);
167
+ if (!self.internetAccounts.some(a => a.internetAccountId === internetAccount.internetAccountId)) {
168
+ self.initializeInternetAccount(internetAccount);
169
+ }
72
170
  }
73
171
  }, { name: 'InternetAccounts' }));
74
172
  },
@@ -0,0 +1,53 @@
1
+ /**
2
+ * #config PreferencesConfigSchema
3
+ * #category root
4
+ * admin/embedder defaults for user-facing preferences, found on the root config
5
+ * as `configuration.preferences`. Individual users override these at runtime
6
+ * (persisted to localStorage) via the session `getPreference` reader; a runtime
7
+ * override map layered over config defaults, at app scope.
8
+ */
9
+ export declare function PreferencesConfigSchemaFactory(): import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaType<{
10
+ /**
11
+ * #slot configuration.preferences.animationMode
12
+ * controls feature-layout animations: 'enabled' always animates (the
13
+ * default), 'system' respects the OS prefers-reduced-motion setting,
14
+ * 'disabled' never animates
15
+ */
16
+ readonly animationMode: {
17
+ readonly model: import("@jbrowse/mobx-state-tree").ISimpleType<"disabled" | "enabled" | "system">;
18
+ readonly type: "stringEnum";
19
+ readonly defaultValue: "enabled";
20
+ };
21
+ /**
22
+ * #slot configuration.preferences.scrollZoom
23
+ * when true, scrolling the mouse wheel over a track zooms in and out
24
+ * without holding Ctrl. Applies globally to all wheel-zoom views.
25
+ */
26
+ readonly scrollZoom: {
27
+ readonly type: "boolean";
28
+ readonly defaultValue: false;
29
+ };
30
+ /**
31
+ * #slot configuration.preferences.numberGrouping
32
+ * when true (the default), numbers are displayed with thousand separators
33
+ * — `chr1:1,234,567`. Turn it off to render them bare, which is what you
34
+ * want if you copy coordinates out of JBrowse into tools that won't accept
35
+ * the commas. Applies to every displayed number, and takes effect on
36
+ * reload.
37
+ */
38
+ readonly numberGrouping: {
39
+ readonly type: "boolean";
40
+ readonly defaultValue: true;
41
+ };
42
+ /**
43
+ * #slot configuration.preferences.useWorkspaces
44
+ * when true, views open in the tabbed/tiled workspace layout rather than
45
+ * stacked vertically. Only the default: a session that names
46
+ * `useWorkspaces` itself (a shared snapshot, or a session spec carrying a
47
+ * `layout`) still wins, and a user's own toggle overrides it.
48
+ */
49
+ readonly useWorkspaces: {
50
+ readonly type: "boolean";
51
+ readonly defaultValue: false;
52
+ };
53
+ }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined>>;