@jbrowse/product-core 4.3.0 → 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,88 +1,379 @@
1
- import { localStorageGetBoolean, localStorageSetBoolean, } from '@jbrowse/core/util';
2
- import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree';
1
+ import { applyOrderWithin, localStorageGetBoolean, localStorageSetBoolean, reorderWithin, } from '@jbrowse/core/util';
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 { BaseSessionModel, isBaseSession } from "./BaseSession.js";
5
- import { DrawerWidgetSessionMixin } from "./DrawerWidgets.js";
4
+ import { scheduleDetachedDestroy } from '../scheduleDetachedDestroy.js';
5
+ import { BaseSessionModel, isBaseSession } from './BaseSession.js';
6
+ import { DrawerWidgetSessionMixin } from './DrawerWidgets.js';
7
+ // Whether `node` IS `view` or lives somewhere under it. A view can CONTAIN
8
+ // views, so `takeOut` matches widgets to the view leaving by containment rather
9
+ // than by `widget.view.id === view.id`. ADR-069 says what that cost.
10
+ //
11
+ // Deliberately not `findParentThat`, which warns and throws when the walk runs
12
+ // out: this asks the question of every reference every widget holds, and most
13
+ // of them point somewhere else entirely, so "no" is an ordinary answer here
14
+ // rather than a fault.
15
+ function isWithin(node, view) {
16
+ if (!isStateTreeNode(node)) {
17
+ return false;
18
+ }
19
+ let cur = node;
20
+ while (cur !== view) {
21
+ if (!hasParent(cur)) {
22
+ return false;
23
+ }
24
+ cur = getParent(cur);
25
+ }
26
+ return true;
27
+ }
28
+ function viewIdsWithin(view) {
29
+ return [view.id, ...(view.views ?? []).flatMap(v => viewIdsWithin(v))];
30
+ }
31
+ // Empty every reference `holder` has into `view`, and say whether it had any.
32
+ //
33
+ // MST invalidates a reference when its TARGET node is detached or destroyed, so
34
+ // a widget's reference to the view itself empties on its way out and one
35
+ // pointing INSIDE it does not: `openFeatureWidget` stores the clicked feature's
36
+ // track, and that node is neither detached nor destroyed — it just leaves the
37
+ // session's identifier cache with the view, which makes reading the reference
38
+ // THROW where `safeReference` promises undefined. The widget's own autoruns go
39
+ // on running whether or not its panel is on screen, so that throw comes out of
40
+ // a reaction, uncatchable by the caller.
41
+ //
42
+ // So do here what `onInvalidated` would have done, while the reference can
43
+ // still be read. Only where the type allows undefined — a required reference
44
+ // has no empty value to move to, and the widget is hidden either way. ADR-069.
45
+ function dropRefsInto(holder, view) {
46
+ let found = false;
47
+ for (const [key, prop] of Object.entries(getMembers(holder).properties)) {
48
+ if (isReferenceType(prop) && isWithin(holder[key], view)) {
49
+ found = true;
50
+ if (prop.is(undefined)) {
51
+ applyPatch(holder, { op: 'replace', path: `/${key}`, value: undefined });
52
+ }
53
+ }
54
+ }
55
+ return found;
56
+ }
57
+ /**
58
+ * #stateModel MultipleViewsSessionMixin
59
+ */
6
60
  export function MultipleViewsSessionMixin(pluginManager) {
7
61
  return types
8
62
  .compose(BaseSessionModel(pluginManager), DrawerWidgetSessionMixin(pluginManager), types.model({
63
+ /**
64
+ * #property
65
+ */
9
66
  views: types.array(pluginManager.pluggableMstType('view', 'stateModel')),
67
+ /**
68
+ * #property
69
+ */
10
70
  stickyViewHeaders: types.optional(types.boolean, () => localStorageGetBoolean('stickyViewHeaders', true)),
11
- useWorkspaces: types.optional(types.boolean, () => localStorageGetBoolean('useWorkspaces', false)),
71
+ /**
72
+ * #property
73
+ * enables the tabbed/tiled workspace layout for this session. Undefined
74
+ * means unspecified — read `effectiveUseWorkspaces`.
75
+ */
76
+ useWorkspaces: types.stripDefault(types.maybe(types.boolean), undefined),
12
77
  }))
13
- .actions(self => ({
14
- moveViewDown(id) {
15
- const idx = self.views.findIndex(v => v.id === id);
16
- if (idx !== -1 && idx < self.views.length - 1) {
17
- self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]);
18
- }
19
- },
20
- moveViewUp(id) {
21
- const idx = self.views.findIndex(view => view.id === id);
22
- if (idx > 0) {
23
- self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]);
24
- }
25
- },
26
- moveViewToTop(id) {
27
- const idx = self.views.findIndex(view => view.id === id);
28
- self.views = cast([
29
- self.views[idx],
30
- ...self.views.filter(view => view.id !== id),
31
- ]);
78
+ .views(self => ({
79
+ /**
80
+ * #getter
81
+ * resolved workspaces flag (never undefined): this session's value, else
82
+ * the user preference over the `configuration.preferences.useWorkspaces`
83
+ * admin default. Every consumer reads this, not the raw property — only
84
+ * sessions built from a snapshot or a spec `layout` set that, so the
85
+ * admin default is what reaches the arrivals that bypass defaultSession.
86
+ */
87
+ get effectiveUseWorkspaces() {
88
+ return (self.useWorkspaces ?? self.getPreference('useWorkspaces') === true);
32
89
  },
33
- moveViewToBottom(id) {
34
- const idx = self.views.findIndex(view => view.id === id);
35
- self.views = cast([
36
- ...self.views.filter(view => view.id !== id),
37
- self.views[idx],
38
- ]);
90
+ /**
91
+ * #getter
92
+ * what `effectiveUseWorkspaces` becomes after `resetUseWorkspaces` — the
93
+ * admin default, with both this session's own value and the user's
94
+ * override out of the way. The Preferences reset diff needs this rather
95
+ * than the override map, which can't see a session-scoped value (a spec
96
+ * `layout`, a "move view to a tab") and so reported nothing to reset.
97
+ */
98
+ get defaultUseWorkspaces() {
99
+ return self.getPreferenceDefault('useWorkspaces') === true;
39
100
  },
40
- addView(typeName, initialState = {}) {
41
- const typeDefinition = pluginManager.getElementType('view', typeName);
42
- if (!typeDefinition) {
43
- throw new Error(`unknown view type ${typeName}`);
101
+ }))
102
+ .actions(self => {
103
+ // `scopeIds` narrows the move to a subset of the stack: in a tabbed
104
+ // workspace "move this view up" means up past the previous view IN THIS
105
+ // PANEL, and the view that happens to precede it in `self.views` may be
106
+ // in another panel, where moving past it would look like a no-op. Omitted
107
+ // (the classic stack) every view is in scope and this is a plain reorder.
108
+ const move = (id, direction, scopeIds) => {
109
+ const idx = self.views.findIndex(v => v.id === id);
110
+ const scope = scopeIds && new Set(scopeIds);
111
+ self.views = cast(reorderWithin(self.views, idx, direction, v => scope ? scope.has(v.id) : true));
112
+ };
113
+ // The body of removeView, callable from replaceView below — an action
114
+ // can't reach a sibling action through `self` from inside the same
115
+ // .actions() block, since neither is attached yet.
116
+ //
117
+ // **Detached, not removed.** `views.remove` destroys the view and
118
+ // everything under it right here, inside the action, and MobX runs the
119
+ // action's pending reactions at the `endBatch` closing it — so every
120
+ // component still mounted over that view gets a final run against nodes
121
+ // that just died. ADR-069.
122
+ //
123
+ // A view is the worse half of that rule rather than the `setSession`
124
+ // half, because what is mounted over it is a DISPLAY, and a display's
125
+ // reads reach `getContainingView`, which walks parents and THROWS when
126
+ // there is none. So this was not only a noisy console: on
127
+ // `cancer_sv/multihop_split_view` the throw went to an ErrorBoundary with
128
+ // no view left under it and took the page. Detaching leaves the walk
129
+ // something to find.
130
+ const takeOut = (view) => {
131
+ // Membership first, and before anything reads through `view`. A view
132
+ // already out of the session reaches here — `replaceView` documents
133
+ // that case below — and by then it is detached, or destroyed by the
134
+ // task scheduled at the bottom. MST's `detach` throws on a node with no
135
+ // parent, and the widgets holding it were emptied when it left.
136
+ if (!self.views.includes(view)) {
137
+ return;
44
138
  }
45
- const length = self.views.push({
46
- ...initialState,
47
- type: typeName,
48
- });
49
- return self.views[length - 1];
50
- },
51
- removeView(view) {
52
- for (const [, widget] of self.activeWidgets) {
53
- if (widget.view?.id === view.id) {
139
+ // The focus is a persisted id, not a reference, so nothing drops it
140
+ // when the view it names goes. Every consumer compares
141
+ // `focusedViewId === view.id`, so a stale one matches nothing — the
142
+ // focus ring vanishes and the drawer stops naming a view, with nothing
143
+ // to say why — and it ships in a saved or shared session. `replaceView`
144
+ // captures `wasFocused` above this and re-points the focus at the
145
+ // replacement afterwards, which is the one case where the focus moves
146
+ // rather than ending.
147
+ const { focusedViewId } = self;
148
+ if (focusedViewId !== undefined &&
149
+ viewIdsWithin(view).includes(focusedViewId)) {
150
+ self.setFocusedViewId(undefined);
151
+ }
152
+ // Every widget, not the active ones: a widget the user closed keeps
153
+ // its references and its autoruns. One that pointed into the view is
154
+ // now empty, so hide it rather than leave the panel showing what it no
155
+ // longer has.
156
+ for (const [, widget] of self.widgets) {
157
+ if (dropRefsInto(widget, view)) {
54
158
  self.hideWidget(widget);
55
159
  }
56
160
  }
57
- self.views.remove(view);
58
- },
59
- setStickyViewHeaders(sticky) {
60
- self.stickyViewHeaders = sticky;
61
- },
62
- setUseWorkspaces(useWorkspaces) {
63
- self.useWorkspaces = useWorkspaces;
64
- },
65
- afterAttach() {
66
- addDisposer(self, autorun(function stickyViewHeadersAutorun() {
67
- localStorageSetBoolean('stickyViewHeaders', self.stickyViewHeaders);
68
- }, { name: 'StickyViewHeaders' }));
69
- addDisposer(self, autorun(function useWorkspacesAutorun() {
70
- localStorageSetBoolean('useWorkspaces', self.useWorkspaces);
71
- }, { name: 'UseWorkspaces' }));
72
- },
73
- }))
161
+ // MST fires the view's `beforeDetach` here, while it is still attached
162
+ // and the walk to the session still resolves. That matters: anything of
163
+ // a view's that reaches OUTSIDE its own tree has to move to that hook,
164
+ // because after this line the view is a root and `getSession` from a
165
+ // root throws. ADR-069's detach-time disposer, at view scope — the
166
+ // comparative views' `releaseTemporaryAssemblies` is the one case.
167
+ detach(view);
168
+ scheduleDetachedDestroy(view);
169
+ };
170
+ return {
171
+ /**
172
+ * #action
173
+ */
174
+ moveViewDown(id, scopeIds) {
175
+ move(id, 'down', scopeIds);
176
+ },
177
+ /**
178
+ * #action
179
+ */
180
+ moveViewUp(id, scopeIds) {
181
+ move(id, 'up', scopeIds);
182
+ },
183
+ /**
184
+ * #action
185
+ */
186
+ moveViewToTop(id, scopeIds) {
187
+ move(id, 'top', scopeIds);
188
+ },
189
+ /**
190
+ * #action
191
+ */
192
+ moveViewToBottom(id, scopeIds) {
193
+ move(id, 'bottom', scopeIds);
194
+ },
195
+ /**
196
+ * #action
197
+ * Put the named views into the given relative order, leaving views not
198
+ * named in their own slots.
199
+ *
200
+ * `session.views` is the one ordering, so this is how a channel that
201
+ * states an order in some other vocabulary gets it applied: a session
202
+ * spec's `layout` names views per panel, top to bottom, and that used to
203
+ * be honoured by the panel assignment array's order. Now the assignment
204
+ * carries membership only, so the layout says it here instead, once.
205
+ */
206
+ orderViews(ids) {
207
+ self.views = cast(applyOrderWithin(self.views, ids, v => v.id));
208
+ },
209
+ /**
210
+ * #action
211
+ */
212
+ addView(typeName, initialState = {}) {
213
+ const length = self.views.push({
214
+ ...initialState,
215
+ type: typeName,
216
+ });
217
+ return self.views[length - 1];
218
+ },
219
+ /**
220
+ * #action
221
+ * swap `view` for a new view of `typeName`, in the slot it occupied.
222
+ *
223
+ * The launchers that build a view of a DIFFERENT type out of one you are
224
+ * already looking at — launch synteny view, read-vs-ref, split view —
225
+ * otherwise append, leaving the source view above the thing it produced
226
+ * and a stack of two views showing the same locus. This is the "replace"
227
+ * half of that offer, and a slot swap rather than remove-then-add so the
228
+ * new view lands where the reader was looking instead of at the bottom of
229
+ * the session.
230
+ *
231
+ * A launcher whose result is the SAME type as its source does not come
232
+ * here and should not: collapse introns offers "Replace current view" by
233
+ * navigating the view it was invoked on, which keeps the view's identity
234
+ * and so lets its snackbar offer an Undo. Swapping in a new node would
235
+ * leave nothing to undo onto.
236
+ *
237
+ * The slot is `session.views`, which is the order views render in under
238
+ * both layout modes (a tab's `viewIds` says which tab a view is in, not
239
+ * where in it), so the swap lands in place either way.
240
+ *
241
+ * WHICH panel is a separate question and this does not answer it: the
242
+ * new view arrives in no tab, and `homeUnassignedViews` puts it in the
243
+ * active panel — the same panel only when the replaced view was in the
244
+ * active one. In practice it is, since clicking into a view activates
245
+ * its panel, and the launch that offers a replace is a click on that
246
+ * view's own menu.
247
+ */
248
+ replaceView(view, typeName, initialState = {}) {
249
+ // read before the removal, which is what makes both stale
250
+ const idx = self.views.indexOf(view);
251
+ // `idx` first, so a view already gone from the session short-circuits
252
+ // before `view.id` is read: that node is detached and, a task later,
253
+ // destroyed — and MST warns on any read through a destroyed one
254
+ const { focusedViewId } = self;
255
+ const wasFocused = idx !== -1 &&
256
+ focusedViewId !== undefined &&
257
+ viewIdsWithin(view).includes(focusedViewId);
258
+ takeOut(view);
259
+ // a view already gone from the session appends, rather than throwing
260
+ // or silently dropping the launch
261
+ const at = idx === -1 ? self.views.length : idx;
262
+ self.views.splice(at, 0, { ...initialState, type: typeName });
263
+ const created = self.views[at];
264
+ // The replacement takes the slot, so it takes the focus with it.
265
+ // Every consumer compares `focusedViewId === view.id`, so a
266
+ // replacement that left the old id in place would simply match
267
+ // nothing: the focus ring would vanish and the drawer would stop
268
+ // naming a view, with nothing to say why.
269
+ if (wasFocused) {
270
+ self.setFocusedViewId(created.id);
271
+ }
272
+ return created;
273
+ },
274
+ /**
275
+ * #action
276
+ */
277
+ removeView(view) {
278
+ takeOut(view);
279
+ },
280
+ /**
281
+ * #action
282
+ * Take out every view the given session snapshot will not keep, before
283
+ * a whole-tree `applySnapshot` destroys it where it stands.
284
+ *
285
+ * Undo and redo are `applySnapshot` on the session
286
+ * (`core/util/TimeTraveller`, which calls this), and every view carries
287
+ * an `ElementId` — so MST reconciles by identifier and destroys what the
288
+ * target lacks, inside the action, with the components still mounted.
289
+ * Measured on a redo across a closed view, with the restored view
290
+ * repainted first: 4 liveliness reads of its display before this, 0
291
+ * after.
292
+ *
293
+ * A view that is present but has changed `type` is destroyed by that
294
+ * same reconciliation (`areSame` runs the type's `is()` before its id
295
+ * check), so identity here is the pair, not the id.
296
+ *
297
+ * **Views, and deliberately not what is under them.** A track or a
298
+ * display the snapshot drops is still destroyed in place, and that is
299
+ * not this door being worse than the others: `hideTrackGeneric` is a
300
+ * plain `tracks.remove` and measures louder (5 reads to undo's 4).
301
+ * Detaching one would be worse than the destroy it replaced — a
302
+ * detached display is a LIVE root, and `getContainingView` walks
303
+ * parents and throws when the walk finds no view, where a dead node
304
+ * only warns. At or above the view is the only place that walk still
305
+ * lands, which is why ADR-069's rule stops there.
306
+ */
307
+ takeOutViewsMissingFrom(snapshot) {
308
+ const incoming = snapshot?.views ?? [];
309
+ const kept = new Map(incoming.map(v => [v.id, v.type]));
310
+ // a copy, because `takeOut` splices the array it is iterating
311
+ for (const view of [...self.views]) {
312
+ if (kept.get(view.id) !== view.type) {
313
+ takeOut(view);
314
+ }
315
+ }
316
+ },
317
+ /**
318
+ * #action
319
+ */
320
+ setStickyViewHeaders(sticky) {
321
+ self.stickyViewHeaders = sticky;
322
+ },
323
+ /**
324
+ * #action
325
+ * set the workspaces layout for this session only, leaving the user's
326
+ * personal default untouched. For session-scoped intent — a spec
327
+ * carrying a `layout`, or an ad-hoc "move view to a tab/split" — where
328
+ * rewriting the visitor's global preference would be a surprise. The
329
+ * user-facing default toggle is `setUseWorkspacesPreference`.
330
+ */
331
+ setUseWorkspaces(useWorkspaces) {
332
+ self.useWorkspaces = useWorkspaces;
333
+ },
334
+ /**
335
+ * #action
336
+ * the user-facing workspaces toggle: applies to this session and
337
+ * becomes their default for sessions that don't specify one. Persisted
338
+ * only here, on an explicit toggle — an autorun mirroring the resolved
339
+ * value would bake the admin default into every visitor's localStorage
340
+ * on first load, so a later admin change could never reach them.
341
+ */
342
+ setUseWorkspacesPreference(useWorkspaces) {
343
+ self.useWorkspaces = useWorkspaces;
344
+ self.setPreferenceOverride('useWorkspaces', useWorkspaces);
345
+ },
346
+ /**
347
+ * #action
348
+ * drop both this session's explicit value and the user's override so
349
+ * workspaces falls back to the admin default
350
+ */
351
+ resetUseWorkspaces() {
352
+ self.useWorkspaces = undefined;
353
+ self.clearPreferenceOverride('useWorkspaces');
354
+ },
355
+ afterAttach() {
356
+ addDisposer(self, autorun(function stickyViewHeadersAutorun() {
357
+ localStorageSetBoolean('stickyViewHeaders', self.stickyViewHeaders);
358
+ }, { name: 'StickyViewHeaders' }));
359
+ },
360
+ };
361
+ })
74
362
  .postProcessSnapshot(snap => {
363
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
75
364
  if (!snap) {
76
365
  return snap;
77
366
  }
78
- const { stickyViewHeaders, useWorkspaces, ...rest } = snap;
79
- return {
80
- ...rest,
81
- ...(!stickyViewHeaders ? { stickyViewHeaders } : {}),
82
- ...(useWorkspaces ? { useWorkspaces } : {}),
83
- };
367
+ // stickyViewHeaders is a personal per-browser UI preference, not shared
368
+ // view state: destructure it out so it never lands in the snapshot. It
369
+ // stays localStorage-backed, so each browser keeps its own value.
370
+ // useWorkspaces stays: it changes layout intent (and pairs with the
371
+ // `layout` tree the snapshot carries), which is meaningful to share.
372
+ const { stickyViewHeaders, ...rest } = snap;
373
+ return rest;
84
374
  });
85
375
  }
376
+ /** Type guard for SessionWithMultipleViews */
86
377
  export function isSessionWithMultipleViews(session) {
87
378
  return isBaseSession(session) && 'views' in session;
88
379
  }