@jbrowse/product-core 4.3.0 → 5.0.0-beta.10

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 (157) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +514 -323
  2. package/esm/RootModel/BaseRootModel.js +141 -14
  3. package/esm/RootModel/HierarchicalConfig.d.ts +30 -20
  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 +37 -0
  8. package/esm/RootModel/PreferencesConfig.js +75 -0
  9. package/esm/RootModel/createConfigModel.d.ts +257 -0
  10. package/esm/RootModel/createConfigModel.js +77 -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 +141 -0
  15. package/esm/Session/BaseSession.d.ts +285 -11
  16. package/esm/Session/BaseSession.js +383 -15
  17. package/esm/Session/Connections.d.ts +115 -8
  18. package/esm/Session/Connections.js +196 -74
  19. package/esm/Session/DrawerWidgets.d.ts +145 -13
  20. package/esm/Session/DrawerWidgets.js +219 -49
  21. package/esm/Session/MultipleViews.d.ts +1067 -1177
  22. package/esm/Session/MultipleViews.js +395 -64
  23. package/esm/Session/Preferences.d.ts +1389 -0
  24. package/esm/Session/Preferences.js +58 -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 +1101 -1180
  28. package/esm/Session/SessionTracks.js +584 -34
  29. package/esm/Session/Themes.d.ts +136 -2
  30. package/esm/Session/Themes.js +267 -21
  31. package/esm/Session/TrackMenu.d.ts +88 -0
  32. package/esm/Session/TrackMenu.js +175 -0
  33. package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
  34. package/esm/Session/TrackMenuSessionMixin.js +42 -0
  35. package/esm/Session/Tracks.d.ts +945 -1159
  36. package/esm/Session/Tracks.js +229 -30
  37. package/esm/Session/assertSessionModel.d.ts +21 -0
  38. package/esm/Session/assertSessionModel.js +1 -0
  39. package/esm/Session/copyTrackSnapshot.d.ts +21 -0
  40. package/esm/Session/copyTrackSnapshot.js +20 -0
  41. package/esm/Session/developerMode.d.ts +7 -0
  42. package/esm/Session/developerMode.js +43 -0
  43. package/esm/Session/finalizeSession.d.ts +15 -0
  44. package/esm/Session/finalizeSession.js +28 -0
  45. package/esm/Session/hydratedForms.d.ts +16 -0
  46. package/esm/Session/hydratedForms.js +13 -0
  47. package/esm/Session/index.d.ts +27 -10
  48. package/esm/Session/index.js +16 -10
  49. package/esm/Session/pluginExtraTrackItems.d.ts +11 -0
  50. package/esm/Session/pluginExtraTrackItems.js +13 -0
  51. package/esm/Session/readdedTrackConf.d.ts +8 -0
  52. package/esm/Session/readdedTrackConf.js +24 -0
  53. package/esm/Session/shareableSnapshot.d.ts +23 -0
  54. package/esm/Session/shareableSnapshot.js +66 -0
  55. package/esm/Session/temporaryAssemblyTracks.d.ts +24 -0
  56. package/esm/Session/temporaryAssemblyTracks.js +37 -0
  57. package/esm/assertExtends.d.ts +55 -0
  58. package/esm/assertExtends.js +1 -0
  59. package/esm/controllerTracks.d.ts +76 -0
  60. package/esm/controllerTracks.js +125 -0
  61. package/esm/destroyViewState.d.ts +21 -0
  62. package/esm/destroyViewState.js +29 -0
  63. package/esm/embeddedController.d.ts +57 -0
  64. package/esm/embeddedController.js +100 -0
  65. package/esm/index.d.ts +32 -4
  66. package/esm/index.js +31 -4
  67. package/esm/loadPlugins.d.ts +38 -0
  68. package/esm/loadPlugins.js +45 -0
  69. package/esm/localFiles.d.ts +74 -0
  70. package/esm/localFiles.js +156 -0
  71. package/esm/observeSession.d.ts +55 -0
  72. package/esm/observeSession.js +90 -0
  73. package/esm/parallelChunkLoading.d.ts +30 -0
  74. package/esm/parallelChunkLoading.js +71 -0
  75. package/esm/pluginInput.d.ts +32 -0
  76. package/esm/pluginInput.js +19 -0
  77. package/esm/pruneUnbuildableNodes.d.ts +109 -0
  78. package/esm/pruneUnbuildableNodes.js +562 -0
  79. package/esm/resolveAssemblies.d.ts +68 -0
  80. package/esm/resolveAssemblies.js +77 -0
  81. package/esm/rpcWorker.d.ts +36 -4
  82. package/esm/rpcWorker.js +126 -20
  83. package/esm/sessionMigrations/index.d.ts +15 -0
  84. package/esm/sessionMigrations/index.js +178 -0
  85. package/esm/sessionUrl.d.ts +40 -0
  86. package/esm/sessionUrl.js +66 -0
  87. package/esm/sessionUtils.d.ts +74 -0
  88. package/esm/sessionUtils.js +599 -0
  89. package/esm/siblingCast.d.ts +14 -0
  90. package/esm/siblingCast.js +16 -0
  91. package/esm/snapshotUtils.d.ts +8 -0
  92. package/esm/snapshotUtils.js +12 -0
  93. package/esm/ui/AboutDialog.d.ts +8 -3
  94. package/esm/ui/AboutDialog.js +14 -5
  95. package/esm/ui/AboutDialogContents.d.ts +2 -6
  96. package/esm/ui/AboutDialogContents.js +13 -17
  97. package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
  98. package/esm/ui/AssemblyInfoPanel.js +63 -0
  99. package/esm/ui/DescriptionPanel.d.ts +3 -0
  100. package/esm/ui/DescriptionPanel.js +25 -0
  101. package/esm/ui/Drawer.d.ts +21 -0
  102. package/esm/ui/Drawer.js +60 -0
  103. package/esm/ui/DrawerControls.d.ts +5 -0
  104. package/esm/ui/DrawerControls.js +28 -0
  105. package/esm/ui/DrawerHeader.d.ts +6 -0
  106. package/esm/ui/DrawerHeader.js +36 -0
  107. package/esm/ui/DrawerWidget.d.ts +5 -0
  108. package/esm/ui/DrawerWidget.js +12 -0
  109. package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
  110. package/esm/ui/DrawerWidgetSelector.js +40 -0
  111. package/esm/ui/FileInfoPanel.d.ts +3 -6
  112. package/esm/ui/FileInfoPanel.js +36 -26
  113. package/esm/ui/GeneralPreferencesTab.d.ts +17 -0
  114. package/esm/ui/GeneralPreferencesTab.js +34 -0
  115. package/esm/ui/HeaderButtons.d.ts +2 -1
  116. package/esm/ui/HeaderButtons.js +15 -17
  117. package/esm/ui/ModalWidget.d.ts +13 -0
  118. package/esm/ui/ModalWidget.js +48 -0
  119. package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
  120. package/esm/ui/ModalWidgetAppBar.js +9 -0
  121. package/esm/ui/PreferenceCheckbox.d.ts +6 -0
  122. package/esm/ui/PreferenceCheckbox.js +16 -0
  123. package/esm/ui/PreferencesDialog.d.ts +34 -0
  124. package/esm/ui/PreferencesDialog.js +55 -0
  125. package/esm/ui/PreferencesResetDialog.d.ts +8 -0
  126. package/esm/ui/PreferencesResetDialog.js +19 -0
  127. package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
  128. package/esm/ui/RefNameAliasesDialog.js +65 -0
  129. package/esm/ui/RefNameInfoDialog.d.ts +3 -6
  130. package/esm/ui/RefNameInfoDialog.js +45 -63
  131. package/esm/ui/ViewPreferencesTab.d.ts +12 -0
  132. package/esm/ui/ViewPreferencesTab.js +15 -0
  133. package/esm/ui/WidgetBody.d.ts +18 -0
  134. package/esm/ui/WidgetBody.js +22 -0
  135. package/esm/ui/WidgetHeading.d.ts +7 -0
  136. package/esm/ui/WidgetHeading.js +7 -0
  137. package/esm/ui/aboutTestUtils.d.ts +140 -0
  138. package/esm/ui/aboutTestUtils.js +25 -0
  139. package/esm/ui/descriptionHtml.d.ts +25 -0
  140. package/esm/ui/descriptionHtml.js +64 -0
  141. package/esm/ui/drawerLayout.d.ts +18 -0
  142. package/esm/ui/drawerLayout.js +22 -0
  143. package/esm/ui/index.d.ts +15 -1
  144. package/esm/ui/index.js +13 -1
  145. package/esm/ui/preferencesReset.d.ts +18 -0
  146. package/esm/ui/preferencesReset.js +69 -0
  147. package/esm/ui/util.d.ts +22 -7
  148. package/esm/ui/util.js +19 -41
  149. package/esm/useEngineLifecycle.d.ts +19 -0
  150. package/esm/useEngineLifecycle.js +44 -0
  151. package/package.json +15 -18
  152. package/esm/RootModel/FormatAbout.d.ts +0 -12
  153. package/esm/RootModel/FormatAbout.js +0 -15
  154. package/esm/RootModel/FormatDetails.d.ts +0 -24
  155. package/esm/RootModel/FormatDetails.js +0 -27
  156. package/esm/Session/DialogQueue.d.ts +0 -17
  157. package/esm/Session/DialogQueue.js +0 -31
@@ -0,0 +1,68 @@
1
+ import type { LooseSearchIndex } from '@jbrowse/core/util/expandLooseSearchIndex';
2
+ export type AssemblyConfig = Record<string, unknown>;
3
+ export interface TextSearchAdapterConfig {
4
+ type: string;
5
+ [key: string]: unknown;
6
+ }
7
+ type SearchAdapters = TextSearchAdapterConfig[] | undefined;
8
+ export interface HubTrackConfig {
9
+ trackId: string;
10
+ [key: string]: unknown;
11
+ }
12
+ /**
13
+ * The shapes an assembly can take, discriminated at resolve time: a sequence
14
+ * file URL (`'.../hg38.fa.gz'`, `.2bit`, ...) built into an assembly via
15
+ * `makeAssembly`; a hub name (`'hg38'`, `'GCF_...'`) fetched from jbrowse.org;
16
+ * a full hub config (as `fetchHub` returns); or a bare assembly config (e.g.
17
+ * from `makeAssembly`) — the latter two both being plain config objects.
18
+ */
19
+ export type AssemblyInput = string | AssemblyConfig;
20
+ export interface ResolvedAssembly {
21
+ assembly: AssemblyConfig;
22
+ /** whatever search adapters the hub this came from carried */
23
+ aggregateTextSearchAdapters?: SearchAdapters;
24
+ /**
25
+ * the hub's track catalog, which its search index names its hits against, so
26
+ * a host keeping the index keeps these too
27
+ */
28
+ tracks?: HubTrackConfig[];
29
+ }
30
+ export interface ResolvedAssemblies<Track extends object = HubTrackConfig, Index extends LooseSearchIndex = TextSearchAdapterConfig> {
31
+ assemblies: AssemblyConfig[];
32
+ /** the hubs' search adapters, then the host's own */
33
+ aggregateTextSearchAdapters?: (TextSearchAdapterConfig | Index)[];
34
+ /** the hubs' track catalogs, then the host's own tracks */
35
+ tracks?: (HubTrackConfig | Track)[];
36
+ }
37
+ /** What a host already has, to merge with what its hubs bring. */
38
+ export interface HostCatalog<Track extends object, Index extends LooseSearchIndex> {
39
+ tracks?: readonly Track[];
40
+ aggregateTextSearchAdapters?: readonly Index[];
41
+ }
42
+ /** One assembly in any of the four accepted shapes. */
43
+ export declare function resolveAssembly(input: AssemblyInput): Promise<ResolvedAssembly>;
44
+ /**
45
+ * Resolve a whole list of assembly inputs, so a multi-assembly product takes
46
+ * the same vocabulary the single-view one does — `['hg38', 'mm39']` as readily
47
+ * as two hand-written configs.
48
+ *
49
+ * A hub brings a track catalog and a search index that names hits by those
50
+ * tracks' ids, so both come back beside the assemblies. Pass the host's own
51
+ * `tracks` and `aggregateTextSearchAdapters` as `host` and they come back merged
52
+ * in, the host's winning an id, which makes spreading the result into
53
+ * `createApp`'s options correct:
54
+ *
55
+ * ```ts
56
+ * createApp(el, { ...options, ...(await resolveAssemblies(names, options)) })
57
+ * ```
58
+ *
59
+ * A key neither the hubs nor the host supplied is left out rather than set to
60
+ * `undefined`, so a spread never clears one.
61
+ *
62
+ * Exported because the resolution is async and `createApp` is not: an engine
63
+ * that resolved its own genomes would have to hand back a controller whose
64
+ * `viewState` is undefined until the fetches land. A host builds its options
65
+ * asynchronously already (runtime plugins), so this belongs in that step.
66
+ */
67
+ export declare function resolveAssemblies<Track extends object = HubTrackConfig, Index extends LooseSearchIndex = TextSearchAdapterConfig>(inputs: AssemblyInput[], host?: HostCatalog<Track, Index>): Promise<ResolvedAssemblies<Track, Index>>;
68
+ export {};
@@ -0,0 +1,77 @@
1
+ import { expandLooseSearchIndex } from '@jbrowse/core/util/expandLooseSearchIndex';
2
+ import { fetchHub } from '@jbrowse/core/util/fetchHub';
3
+ import { isSequenceUri, makeAssembly } from '@jbrowse/core/util/makeAssembly';
4
+ import { searchIndexKey, withHostOverrides } from './controllerTracks.js';
5
+ // A hub is one assembly, so an index it writes short is on that assembly: it is
6
+ // expanded here, before the merge puts it beside other hubs' assemblies where no
7
+ // single one would fill it. What still names no adapter type cannot be
8
+ // constructed, so it is dropped rather than cast.
9
+ function searchAdaptersOf(hub) {
10
+ const name = hub.assemblies?.[0]?.name;
11
+ const found = (hub.aggregateTextSearchAdapters ?? [])
12
+ .map(index => expandLooseSearchIndex(index, typeof name === 'string' ? [name] : []))
13
+ .filter((a) => typeof a?.type === 'string');
14
+ return found.length ? found : undefined;
15
+ }
16
+ function tracksOf(hub) {
17
+ const found = (hub.tracks ?? []).filter((t) => typeof t.trackId === 'string');
18
+ return found.length ? found : undefined;
19
+ }
20
+ function fromHubConfig(hub) {
21
+ const assembly = hub.assemblies?.[0];
22
+ if (!assembly) {
23
+ throw new Error('hub config has no assemblies');
24
+ }
25
+ return {
26
+ assembly,
27
+ aggregateTextSearchAdapters: searchAdaptersOf(hub),
28
+ tracks: tracksOf(hub),
29
+ };
30
+ }
31
+ /** One assembly in any of the four accepted shapes. */
32
+ export async function resolveAssembly(input) {
33
+ if (typeof input === 'string') {
34
+ return isSequenceUri(input)
35
+ ? { assembly: makeAssembly({ fastaUri: input }) }
36
+ : fromHubConfig(await fetchHub(input));
37
+ }
38
+ else if ('assemblies' in input) {
39
+ return fromHubConfig(input);
40
+ }
41
+ else {
42
+ return { assembly: input };
43
+ }
44
+ }
45
+ /**
46
+ * Resolve a whole list of assembly inputs, so a multi-assembly product takes
47
+ * the same vocabulary the single-view one does — `['hg38', 'mm39']` as readily
48
+ * as two hand-written configs.
49
+ *
50
+ * A hub brings a track catalog and a search index that names hits by those
51
+ * tracks' ids, so both come back beside the assemblies. Pass the host's own
52
+ * `tracks` and `aggregateTextSearchAdapters` as `host` and they come back merged
53
+ * in, the host's winning an id, which makes spreading the result into
54
+ * `createApp`'s options correct:
55
+ *
56
+ * ```ts
57
+ * createApp(el, { ...options, ...(await resolveAssemblies(names, options)) })
58
+ * ```
59
+ *
60
+ * A key neither the hubs nor the host supplied is left out rather than set to
61
+ * `undefined`, so a spread never clears one.
62
+ *
63
+ * Exported because the resolution is async and `createApp` is not: an engine
64
+ * that resolved its own genomes would have to hand back a controller whose
65
+ * `viewState` is undefined until the fetches land. A host builds its options
66
+ * asynchronously already (runtime plugins), so this belongs in that step.
67
+ */
68
+ export async function resolveAssemblies(inputs, host = {}) {
69
+ const resolved = await Promise.all(inputs.map(resolveAssembly));
70
+ const tracks = withHostOverrides(resolved.flatMap(r => r.tracks ?? []), host.tracks, 'trackId');
71
+ const adapters = withHostOverrides(resolved.flatMap(r => r.aggregateTextSearchAdapters ?? []), host.aggregateTextSearchAdapters, searchIndexKey);
72
+ return {
73
+ assemblies: resolved.map(r => r.assembly),
74
+ ...(tracks.length ? { tracks } : {}),
75
+ ...(adapters.length ? { aggregateTextSearchAdapters: adapters } : {}),
76
+ };
77
+ }
@@ -1,12 +1,44 @@
1
1
  import { RpcServer } from '@jbrowse/core/util/librpc';
2
2
  import type { PluginConstructor } from '@jbrowse/core/Plugin';
3
- import type { LoadedPlugin } from '@jbrowse/core/PluginLoader';
3
+ import type { LoadedPlugin, ReExportRegistryLoader } from '@jbrowse/core/PluginLoader';
4
4
  declare global {
5
5
  interface Window {
6
6
  rpcServer?: RpcServer;
7
7
  }
8
8
  }
9
- export declare function initializeWorker(corePlugins: PluginConstructor[], opts: {
9
+ export interface WorkerOptions {
10
10
  fetchESM?: (url: string) => Promise<LoadedPlugin>;
11
- fetchCJS?: (url: string) => Promise<LoadedPlugin>;
12
- }): Promise<void>;
11
+ /**
12
+ * The product's `workerReExports.generated.ts`, so a runtime plugin loading
13
+ * here reads the host's copy of every `@jbrowse` package the product
14
+ * bundles.
15
+ */
16
+ reExports: ReExportRegistryLoader;
17
+ }
18
+ type RpcFunc = (args: unknown) => Promise<unknown>;
19
+ /**
20
+ * Wire the method's `statusCallback` to the status channel the driver opened
21
+ * for this call — and to nothing at all when it opened none.
22
+ *
23
+ * The `channel` is the driver's statement of whether anyone is listening
24
+ * (`WebWorkerHandle.call`), so a method run without one has to see the
25
+ * `undefined` its caller passed. Injecting a callback regardless made every
26
+ * "no statusCallback" branch in the worker unreachable: the byte reporter an
27
+ * adapter hands its reader, the emit `createProgressReporter` gates, and the
28
+ * postMessage each of those cost to reach a main-thread listener that dropped
29
+ * it.
30
+ *
31
+ * `channel` is transport bookkeeping and comes back OFF here, the mirror of
32
+ * `BaseRpcDriver.call` stripping `statusCallback` on the way out. It used to
33
+ * ride through into every worker method's arguments, where nothing read it and
34
+ * `RpcExecuteArgs<M>` did not declare it — a key the main-thread driver never
35
+ * adds, so the two drivers handed `execute` different bags. `rpcDriverParity`
36
+ * is what says so.
37
+ *
38
+ * Exported for its test and not from the package barrel: `initializeWorker` is
39
+ * the only thing a product's worker entry calls, and the branch here is a claim
40
+ * about the driver on the other side of postMessage that nothing else asserts.
41
+ */
42
+ export declare function wrapForRpc(func: RpcFunc): (args: unknown) => Promise<unknown>;
43
+ export declare function initializeWorker(corePlugins: PluginConstructor[], opts: WorkerOptions): Promise<void>;
44
+ export {};
package/esm/rpcWorker.js CHANGED
@@ -1,13 +1,34 @@
1
- import PluginLoader from '@jbrowse/core/PluginLoader';
1
+ import PluginLoader, { preloadUMDBundles } from '@jbrowse/core/PluginLoader';
2
2
  import PluginManager from '@jbrowse/core/PluginManager';
3
+ import { setNumberGrouping, throttleStatusEmits } from '@jbrowse/core/util';
3
4
  import { RpcServer, serializeError } from '@jbrowse/core/util/librpc';
4
- function receiveConfiguration() {
5
+ import { setStackTraceLimit } from '@jbrowse/core/util/setStackTraceLimit';
6
+ import { parallelizeWorkerChunkLoading } from './parallelChunkLoading.js';
7
+ /**
8
+ * Start downloading what a boot configuration naming these plugins will load.
9
+ * A page that started this worker early sends the hint once it has resolved
10
+ * its plugins, which is well before it can send the configuration: that waits
11
+ * for the page to evaluate them and build its session. The configuration is
12
+ * still what the worker loads.
13
+ */
14
+ function preloadHintedPlugins({ plugins, windowHref }, opts) {
15
+ if (plugins.length) {
16
+ opts.reExports().catch(() => { });
17
+ preloadUMDBundles(plugins, windowHref);
18
+ }
19
+ }
20
+ // waits for a message from the main thread containing our configuration, which
21
+ // must be sent on boot
22
+ function receiveConfiguration(opts) {
5
23
  const configurationP = new Promise(resolve => {
6
24
  function listener(e) {
7
- if (e.data.message === 'config') {
25
+ if (e.data.message === 'config' && e.data.config) {
8
26
  resolve(e.data.config);
9
27
  removeEventListener('message', listener);
10
28
  }
29
+ else if (e.data.message === 'preloadPlugins' && e.data.hint) {
30
+ preloadHintedPlugins(e.data.hint, opts);
31
+ }
11
32
  }
12
33
  self.addEventListener('message', listener);
13
34
  });
@@ -15,43 +36,128 @@ function receiveConfiguration() {
15
36
  return configurationP;
16
37
  }
17
38
  async function getPluginManager(corePlugins, opts) {
18
- const config = await receiveConfiguration();
19
- const pluginLoader = new PluginLoader(config.plugins, opts).installGlobalReExports(self);
39
+ const config = await receiveConfiguration(opts);
40
+ // this realm formats its own strings — a jexl `mouseover` slot renders a
41
+ // tooltip against the full feature here rather than shipping it back — so it
42
+ // needs the main thread's display preference before any RPC method runs
43
+ setNumberGrouping(config.numberGrouping);
44
+ // Keep each runtime plugin's `definition` on its load record (mirroring the
45
+ // main thread's createPluginManager) so PluginManager populates
46
+ // runtimePluginDefinitions. A plugin that resolves a sibling asset from its
47
+ // own bundle url in the worker — e.g. GraphGenomeView's Bandage layout
48
+ // engine — needs its definition here, not just its instance.
49
+ const runtimePlugins = config.plugins.length
50
+ ? await new PluginLoader(config.plugins, opts)
51
+ .installGlobalReExports(self, opts.reExports)
52
+ .load(config.windowHref)
53
+ : [];
20
54
  return new PluginManager([
21
- ...corePlugins.map(p => ({ plugin: p })),
22
- ...(await pluginLoader.load(config.windowHref)),
23
- ].map(P => new P.plugin()))
55
+ ...corePlugins.map(P => ({ plugin: new P() })),
56
+ ...runtimePlugins.map(({ plugin: P, definition }) => ({
57
+ plugin: new P(),
58
+ definition,
59
+ })),
60
+ ])
24
61
  .createPluggableElements()
25
62
  .configure();
26
63
  }
27
- function wrapForRpc(func) {
64
+ /**
65
+ * Wire the method's `statusCallback` to the status channel the driver opened
66
+ * for this call — and to nothing at all when it opened none.
67
+ *
68
+ * The `channel` is the driver's statement of whether anyone is listening
69
+ * (`WebWorkerHandle.call`), so a method run without one has to see the
70
+ * `undefined` its caller passed. Injecting a callback regardless made every
71
+ * "no statusCallback" branch in the worker unreachable: the byte reporter an
72
+ * adapter hands its reader, the emit `createProgressReporter` gates, and the
73
+ * postMessage each of those cost to reach a main-thread listener that dropped
74
+ * it.
75
+ *
76
+ * `channel` is transport bookkeeping and comes back OFF here, the mirror of
77
+ * `BaseRpcDriver.call` stripping `statusCallback` on the way out. It used to
78
+ * ride through into every worker method's arguments, where nothing read it and
79
+ * `RpcExecuteArgs<M>` did not declare it — a key the main-thread driver never
80
+ * adds, so the two drivers handed `execute` different bags. `rpcDriverParity`
81
+ * is what says so.
82
+ *
83
+ * Exported for its test and not from the package barrel: `initializeWorker` is
84
+ * the only thing a product's worker entry calls, and the branch here is a claim
85
+ * about the driver on the other side of postMessage that nothing else asserts.
86
+ */
87
+ export function wrapForRpc(func) {
28
88
  return (args) => {
29
- const wrappedArgs = args;
30
- const { channel, rpcDriverClassName } = wrappedArgs;
31
- return func({
32
- ...wrappedArgs,
33
- statusCallback: (message) => {
34
- self.rpcServer?.emit(channel, message);
35
- },
36
- }, rpcDriverClassName);
89
+ const { channel, ...rest } = args;
90
+ return func(channel === undefined
91
+ ? rest
92
+ : {
93
+ ...rest,
94
+ // thinned here rather than only where it lands: every emit costs a
95
+ // postMessage, a task and a clone on the main thread, which then
96
+ // throttles the same stream again to decide what to draw
97
+ statusCallback: throttleStatusEmits((message) => {
98
+ self.rpcServer?.emit(channel, message);
99
+ }),
100
+ });
37
101
  };
38
102
  }
39
103
  export async function initializeWorker(corePlugins, opts) {
104
+ // a worker error's stack is captured here, not on the main thread, so the
105
+ // frame limit has to be raised in this scope to deepen it
106
+ setStackTraceLimit();
107
+ parallelizeWorkerChunkLoading();
40
108
  self.addEventListener('error', event => {
41
109
  console.error('[Worker uncaught error]', event.error || event.message);
42
110
  });
43
111
  self.addEventListener('unhandledrejection', event => {
44
112
  console.error('[Worker unhandled rejection]', event.reason);
45
113
  });
114
+ // the page posts calls without waiting for 'ready', and only the server
115
+ // built once the plugins have loaded can answer them
116
+ const early = [];
117
+ const hold = (e) => {
118
+ if (e.data.libRpc) {
119
+ early.push(e);
120
+ }
121
+ };
122
+ self.addEventListener('message', hold);
46
123
  try {
47
124
  const pluginManager = await getPluginManager(corePlugins, opts);
48
125
  const rpcConfig = Object.fromEntries(pluginManager
49
126
  .getRpcElements()
50
- .map(e => [e.name, wrapForRpc(e.execute.bind(e))]));
51
- self.rpcServer = new RpcServer(rpcConfig);
127
+ // `invoke`, not `execute`: it deserializes the arguments before handing
128
+ // them on, which is what makes the blob map current and the declared
129
+ // arg types true. MainThreadRpcDriver binds the same one.
130
+ .map(e => [e.name, wrapForRpc(e.invoke.bind(e))]));
131
+ const server = new RpcServer(rpcConfig);
132
+ self.rpcServer = server;
133
+ self.removeEventListener('message', hold);
134
+ for (const e of early) {
135
+ server.handler(e);
136
+ }
52
137
  postMessage({ message: 'ready' });
53
138
  }
54
139
  catch (e) {
55
- postMessage({ message: 'error', error: serializeError(e) });
140
+ // The boot handshake's last frame, and it must never fail to send — for the
141
+ // reason RpcServer.throw must not, one level worse. `serializeError` copies
142
+ // an error's own-enumerable properties and skips only the ones that are
143
+ // themselves functions, so an error carrying an object with a method on it
144
+ // (a `cause` that is a Response, an adapter instance) makes this postMessage
145
+ // throw DataCloneError. This runs floating, so that throw becomes an
146
+ // unhandled rejection the listener above only logs: no 'ready', no 'error',
147
+ // and no ErrorEvent either, since a rejected promise is not a script error.
148
+ // The boot promise in WebWorkerRpcDriver then never settles, and because
149
+ // `invalidate` hangs off its rejection, the pool hands every later caller
150
+ // the same pending promise — every RPC on that slot awaits forever, showing
151
+ // a spinner and no error, until the page is reloaded. One worker's failure
152
+ // to report a failure takes the whole session's worth of calls with it.
153
+ //
154
+ // deserializeError accepts a bare string, which is what makes the fallback
155
+ // sound: a string always clones.
156
+ try {
157
+ postMessage({ message: 'error', error: serializeError(e) });
158
+ }
159
+ catch {
160
+ postMessage({ message: 'error', error: `${e}` });
161
+ }
56
162
  }
57
163
  }
@@ -0,0 +1,15 @@
1
+ import type PluginManager from '@jbrowse/core/PluginManager';
2
+ /**
3
+ * Every display-instance key an old session may carry and have honoured, by
4
+ * display type, `*` for any display, so `jbrowse validate` reports one as
5
+ * stale rather than dead.
6
+ */
7
+ export declare function migratedDisplayInstanceKeys(pluginManager: PluginManager): Record<string, string[]>;
8
+ /**
9
+ * Returns the snapshot by identity when nothing in it is retired, which is
10
+ * every session this build wrote.
11
+ *
12
+ * An app session holds `views`; a single-view embedded product's session holds
13
+ * one `view`, and a share link written by either reaches both.
14
+ */
15
+ export declare function migrateSessionSnapshot(snapshot: Record<string, unknown>, pluginManager: PluginManager): Record<string, unknown>;
@@ -0,0 +1,178 @@
1
+ import { getConfigurationSchemaMetadata, liftRetiredSpellings, } from '@jbrowse/core/configuration';
2
+ function resolverFor(pluginManager) {
3
+ const byName = new Map();
4
+ for (const display of pluginManager.getDisplayElements()) {
5
+ byName.set(display.name, { display });
6
+ for (const retired of display.retiredTypes) {
7
+ byName.set(retired.type, { display, retired });
8
+ }
9
+ }
10
+ return type => (typeof type === 'string' ? byName.get(type) : undefined);
11
+ }
12
+ /**
13
+ * Every display-instance key an old session may carry and have honoured, by
14
+ * display type, `*` for any display, so `jbrowse validate` reports one as
15
+ * stale rather than dead.
16
+ */
17
+ export function migratedDisplayInstanceKeys(pluginManager) {
18
+ return {
19
+ '*': ['heightPreConfig'],
20
+ ...Object.fromEntries(pluginManager
21
+ .getDisplayElements()
22
+ .flatMap(d => d.retiredState ? [[d.name, [...d.retiredState.keys]]] : [])),
23
+ };
24
+ }
25
+ function isObject(v) {
26
+ return !!v && typeof v === 'object' && !Array.isArray(v);
27
+ }
28
+ function migrateDisplay(display, trackConfigId, resolve, collected) {
29
+ const resolved = resolve(display.type);
30
+ if (!resolved) {
31
+ return display;
32
+ }
33
+ const { display: displayType, retired } = resolved;
34
+ const state = displayType.retiredState;
35
+ const shed = ['heightPreConfig', ...(state?.keys ?? [])].filter(k => k in display);
36
+ if (!retired && shed.length === 0) {
37
+ return display;
38
+ }
39
+ const { configuration } = display;
40
+ if (typeof configuration !== 'string') {
41
+ return { ...display, type: displayType.name };
42
+ }
43
+ const lifted = {
44
+ ...state?.lift(display),
45
+ ...(typeof display.heightPreConfig === 'number'
46
+ ? { height: display.heightPreConfig }
47
+ : {}),
48
+ };
49
+ const migrated = retired?.migrate?.(lifted) ?? lifted;
50
+ const meta = getConfigurationSchemaMetadata(displayType.configSchema);
51
+ const settings = meta ? liftRetiredSpellings(meta, migrated) : migrated;
52
+ const displayId = retired && configuration === `${trackConfigId}-${retired.type}`
53
+ ? `${trackConfigId}-${displayType.name}`
54
+ : configuration;
55
+ if (trackConfigId && Object.keys(settings).length > 0) {
56
+ collected.push({
57
+ trackConfigId,
58
+ displayId,
59
+ displayType: displayType.name,
60
+ settings,
61
+ });
62
+ }
63
+ const rest = { ...display };
64
+ for (const key of shed) {
65
+ delete rest[key];
66
+ }
67
+ return { ...rest, type: displayType.name, configuration: displayId };
68
+ }
69
+ function migrateTrack(track, resolve, collected) {
70
+ if (!isObject(track) || !Array.isArray(track.displays)) {
71
+ return track;
72
+ }
73
+ const trackConfigId = typeof track.configuration === 'string' ? track.configuration : undefined;
74
+ const displays = track.displays;
75
+ const next = displays.map(d => isObject(d) ? migrateDisplay(d, trackConfigId, resolve, collected) : d);
76
+ return next.some((d, i) => d !== displays[i])
77
+ ? { ...track, displays: next }
78
+ : track;
79
+ }
80
+ function migrateView(view, resolve, collected) {
81
+ let result = view;
82
+ const { tracks, views } = view;
83
+ if (Array.isArray(tracks)) {
84
+ const next = tracks.map(t => migrateTrack(t, resolve, collected));
85
+ if (next.some((t, i) => t !== tracks[i])) {
86
+ result = { ...result, tracks: next };
87
+ }
88
+ }
89
+ // a synteny view's panels are views of their own
90
+ if (Array.isArray(views)) {
91
+ const next = views.map(v => isObject(v) ? migrateView(v, resolve, collected) : v);
92
+ if (next.some((v, i) => v !== views[i])) {
93
+ result = { ...result, views: next };
94
+ }
95
+ }
96
+ return result;
97
+ }
98
+ // The entry the settings belong to is the one under their id, else the one
99
+ // whose type loads as the same display: a session track still spelling the
100
+ // retired type holds it under the retired type's id.
101
+ function mergeSettingsIntoTrackConfig(track, { displayId, displayType, settings }, resolve) {
102
+ const list = Array.isArray(track.displays)
103
+ ? track.displays
104
+ : [];
105
+ const index = [
106
+ (d) => isObject(d) && d.displayId === displayId,
107
+ (d) => isObject(d) && resolve(d.type)?.display.name === displayType,
108
+ ]
109
+ .map(matches => list.findIndex(matches))
110
+ .find(i => i !== -1);
111
+ const displays = index === undefined
112
+ ? [...list, { type: displayType, displayId, ...settings }]
113
+ : list.map((d, i) => i === index ? { ...d, ...settings } : d);
114
+ return { ...track, displays };
115
+ }
116
+ function applyExtractedSettings(snapshot, collected, resolve) {
117
+ if (collected.length === 0) {
118
+ return snapshot;
119
+ }
120
+ const sessionTracks = Array.isArray(snapshot.sessionTracks)
121
+ ? [...snapshot.sessionTracks]
122
+ : [];
123
+ const sessionTrackIndex = new Map(sessionTracks.map((t, i) => [isObject(t) ? t.trackId : undefined, i]));
124
+ const deltas = isObject(snapshot.trackConfigDeltas)
125
+ ? { ...snapshot.trackConfigDeltas }
126
+ : {};
127
+ let sessionTracksChanged = false;
128
+ let deltasChanged = false;
129
+ for (const extracted of collected) {
130
+ const { trackConfigId } = extracted;
131
+ const index = sessionTrackIndex.get(trackConfigId);
132
+ const sessionTrack = index === undefined ? undefined : sessionTracks[index];
133
+ if (index !== undefined && isObject(sessionTrack)) {
134
+ sessionTracks[index] = mergeSettingsIntoTrackConfig(sessionTrack, extracted, resolve);
135
+ sessionTracksChanged = true;
136
+ }
137
+ else {
138
+ const existing = deltas[trackConfigId];
139
+ deltas[trackConfigId] = mergeSettingsIntoTrackConfig(isObject(existing) ? existing : { trackId: trackConfigId }, extracted, resolve);
140
+ deltasChanged = true;
141
+ }
142
+ }
143
+ return {
144
+ ...snapshot,
145
+ ...(sessionTracksChanged ? { sessionTracks } : {}),
146
+ ...(deltasChanged ? { trackConfigDeltas: deltas } : {}),
147
+ };
148
+ }
149
+ /**
150
+ * Returns the snapshot by identity when nothing in it is retired, which is
151
+ * every session this build wrote.
152
+ *
153
+ * An app session holds `views`; a single-view embedded product's session holds
154
+ * one `view`, and a share link written by either reaches both.
155
+ */
156
+ export function migrateSessionSnapshot(snapshot, pluginManager) {
157
+ const { views, view } = snapshot;
158
+ const many = Array.isArray(views);
159
+ if (!many && !isObject(view)) {
160
+ return snapshot;
161
+ }
162
+ const resolve = resolverFor(pluginManager);
163
+ const collected = [];
164
+ let result = snapshot;
165
+ if (many) {
166
+ const next = views.map(v => isObject(v) ? migrateView(v, resolve, collected) : v);
167
+ if (next.some((v, i) => v !== views[i])) {
168
+ result = { ...result, views: next };
169
+ }
170
+ }
171
+ if (isObject(view)) {
172
+ const next = migrateView(view, resolve, collected);
173
+ if (next !== view) {
174
+ result = { ...result, view: next };
175
+ }
176
+ }
177
+ return applyExtractedSettings(result, collected, resolve);
178
+ }
@@ -0,0 +1,40 @@
1
+ import type { AbstractSessionModel } from '@jbrowse/core/util';
2
+ /**
3
+ * A serialized session. Open-shaped on purpose: the concrete shape is the
4
+ * session model's `SnapshotIn`, which grows with every plugin a host loads.
5
+ */
6
+ export interface SessionSnapshot {
7
+ name: string;
8
+ [key: string]: unknown;
9
+ }
10
+ /**
11
+ * Serialize a live session into a compact, URL-safe string for a query param or
12
+ * hash fragment. Deflated then base64url-encoded, so it survives a URL intact
13
+ * and stays far smaller than raw JSON.
14
+ *
15
+ * Not a plain `getSnapshot`: a workspace arrangement turned on by the sender's
16
+ * preference is not in the snapshot, so a raw one renders as a classic stack
17
+ * for whoever opens the link. `getShareableSessionSnapshot` stamps it first.
18
+ *
19
+ * The session travels but the config does not — the receiving app supplies its
20
+ * own assembly/tracks. Put the result somewhere the server never sees (the hash
21
+ * fragment) if it may be long: a query string can exceed the request-line limit
22
+ * and get a 414, which is why jbrowse-web moved its own there.
23
+ *
24
+ * Carries `ENCODED_PREFIX`, the same `?session=encoded-…` value jbrowse-web's
25
+ * SessionLoader decodes, so a link built by an embedded product opens in
26
+ * jbrowse.org/jb2 and vice versa. The format is `encodeSessionParam`'s `long`
27
+ * mode; sessionUrl.test.ts asserts the two stay byte-identical.
28
+ */
29
+ export declare function encodeSessionToUrl(session: AbstractSessionModel): Promise<string>;
30
+ /**
31
+ * Inverse of {@link encodeSessionToUrl}, and of the `json-` form jbrowse-web's
32
+ * share dialog writes. Accepts an `encoded-` value with or without its prefix,
33
+ * so a raw `?session=` value from either app passes straight through.
34
+ *
35
+ * Throws on anything that isn't a decodable session — a truncated link, or a
36
+ * `share-`/`spec-` param an embedded product doesn't handle — so a host can
37
+ * fall back to its own declarative launch state instead of opening a
38
+ * half-built session.
39
+ */
40
+ export declare function decodeSessionFromUrl(value: string): Promise<SessionSnapshot>;