@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.
- package/esm/RootModel/BaseRootModel.d.ts +629 -304
- package/esm/RootModel/BaseRootModel.js +138 -14
- package/esm/RootModel/HierarchicalConfig.d.ts +57 -21
- package/esm/RootModel/HierarchicalConfig.js +36 -0
- package/esm/RootModel/InternetAccounts.d.ts +18 -1
- package/esm/RootModel/InternetAccounts.js +120 -22
- package/esm/RootModel/PreferencesConfig.d.ts +53 -0
- package/esm/RootModel/PreferencesConfig.js +61 -0
- package/esm/RootModel/createConfigModel.d.ts +232 -0
- package/esm/RootModel/createConfigModel.js +56 -0
- package/esm/RootModel/index.d.ts +10 -5
- package/esm/RootModel/index.js +10 -5
- package/esm/RootModel/menuItems.d.ts +24 -0
- package/esm/RootModel/menuItems.js +143 -0
- package/esm/Session/BaseSession.d.ts +310 -11
- package/esm/Session/BaseSession.js +448 -14
- package/esm/Session/Connections.d.ts +114 -8
- package/esm/Session/Connections.js +195 -74
- package/esm/Session/DrawerWidgets.d.ts +115 -12
- package/esm/Session/DrawerWidgets.js +161 -42
- package/esm/Session/MultipleViews.d.ts +1266 -1128
- package/esm/Session/MultipleViews.js +357 -66
- package/esm/Session/Preferences.d.ts +1665 -0
- package/esm/Session/Preferences.js +54 -0
- package/esm/Session/ReferenceManagement.d.ts +28 -3
- package/esm/Session/ReferenceManagement.js +56 -41
- package/esm/Session/SessionTracks.d.ts +1275 -1138
- package/esm/Session/SessionTracks.js +532 -35
- package/esm/Session/Themes.d.ts +87 -2
- package/esm/Session/Themes.js +156 -23
- package/esm/Session/TrackMenu.d.ts +112 -0
- package/esm/Session/TrackMenu.js +200 -0
- package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
- package/esm/Session/TrackMenuSessionMixin.js +41 -0
- package/esm/Session/Tracks.d.ts +1169 -1117
- package/esm/Session/Tracks.js +160 -20
- package/esm/Session/assertSessionModel.d.ts +21 -0
- package/esm/Session/assertSessionModel.js +1 -0
- package/esm/Session/finalizeSession.d.ts +16 -0
- package/esm/Session/finalizeSession.js +29 -0
- package/esm/Session/index.d.ts +22 -10
- package/esm/Session/index.js +13 -10
- package/esm/Session/shareableSnapshot.d.ts +77 -0
- package/esm/Session/shareableSnapshot.js +196 -0
- package/esm/Session/temporaryAssemblyTracks.d.ts +23 -0
- package/esm/Session/temporaryAssemblyTracks.js +38 -0
- package/esm/assertExtends.d.ts +55 -0
- package/esm/assertExtends.js +1 -0
- package/esm/controllerTracks.d.ts +56 -0
- package/esm/controllerTracks.js +77 -0
- package/esm/destroyViewState.d.ts +21 -0
- package/esm/destroyViewState.js +29 -0
- package/esm/index.d.ts +31 -4
- package/esm/index.js +31 -4
- package/esm/loadPlugins.d.ts +33 -0
- package/esm/loadPlugins.js +38 -0
- package/esm/localFiles.d.ts +74 -0
- package/esm/localFiles.js +156 -0
- package/esm/observeSession.d.ts +56 -0
- package/esm/observeSession.js +91 -0
- package/esm/pluginInput.d.ts +32 -0
- package/esm/pluginInput.js +19 -0
- package/esm/pruneUnbuildableNodes.d.ts +109 -0
- package/esm/pruneUnbuildableNodes.js +568 -0
- package/esm/resolveAssemblies.d.ts +44 -0
- package/esm/resolveAssemblies.js +55 -0
- package/esm/rpcWorker.d.ts +26 -0
- package/esm/rpcWorker.js +96 -15
- package/esm/scheduleDetachedDestroy.d.ts +18 -0
- package/esm/scheduleDetachedDestroy.js +24 -0
- package/esm/sessionMigrations/index.d.ts +53 -0
- package/esm/sessionMigrations/index.js +432 -0
- package/esm/sessionUrl.d.ts +41 -0
- package/esm/sessionUrl.js +53 -0
- package/esm/sessionUtils.d.ts +69 -0
- package/esm/sessionUtils.js +613 -0
- package/esm/siblingCast.d.ts +14 -0
- package/esm/siblingCast.js +16 -0
- package/esm/snapshotUtils.d.ts +8 -0
- package/esm/snapshotUtils.js +12 -0
- package/esm/ui/AboutDialog.d.ts +3 -6
- package/esm/ui/AboutDialog.js +4 -4
- package/esm/ui/AboutDialogContents.d.ts +2 -6
- package/esm/ui/AboutDialogContents.js +34 -13
- package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
- package/esm/ui/AssemblyInfoPanel.js +69 -0
- package/esm/ui/DisplayDefaultsSection.d.ts +36 -0
- package/esm/ui/DisplayDefaultsSection.js +65 -0
- package/esm/ui/Drawer.d.ts +21 -0
- package/esm/ui/Drawer.js +60 -0
- package/esm/ui/DrawerControls.d.ts +5 -0
- package/esm/ui/DrawerControls.js +28 -0
- package/esm/ui/DrawerHeader.d.ts +6 -0
- package/esm/ui/DrawerHeader.js +36 -0
- package/esm/ui/DrawerWidget.d.ts +5 -0
- package/esm/ui/DrawerWidget.js +12 -0
- package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
- package/esm/ui/DrawerWidgetSelector.js +40 -0
- package/esm/ui/FileInfoPanel.d.ts +3 -6
- package/esm/ui/FileInfoPanel.js +35 -26
- package/esm/ui/HeaderButtons.d.ts +7 -1
- package/esm/ui/HeaderButtons.js +23 -18
- package/esm/ui/ModalWidget.d.ts +12 -0
- package/esm/ui/ModalWidget.js +33 -0
- package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
- package/esm/ui/ModalWidgetAppBar.js +9 -0
- package/esm/ui/PreferencesDialog.d.ts +56 -0
- package/esm/ui/PreferencesDialog.js +145 -0
- package/esm/ui/PreferencesResetDialog.d.ts +8 -0
- package/esm/ui/PreferencesResetDialog.js +19 -0
- package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
- package/esm/ui/RefNameAliasesDialog.js +65 -0
- package/esm/ui/RefNameInfoDialog.d.ts +3 -6
- package/esm/ui/RefNameInfoDialog.js +57 -63
- package/esm/ui/WidgetBody.d.ts +18 -0
- package/esm/ui/WidgetBody.js +22 -0
- package/esm/ui/WidgetHeading.d.ts +7 -0
- package/esm/ui/WidgetHeading.js +7 -0
- package/esm/ui/drawerLayout.d.ts +18 -0
- package/esm/ui/drawerLayout.js +22 -0
- package/esm/ui/index.d.ts +13 -1
- package/esm/ui/index.js +12 -1
- package/esm/ui/util.d.ts +37 -4
- package/esm/ui/util.js +35 -38
- package/esm/useEngineLifecycle.d.ts +13 -0
- package/esm/useEngineLifecycle.js +22 -0
- package/package.json +11 -15
- package/esm/RootModel/FormatAbout.d.ts +0 -12
- package/esm/RootModel/FormatAbout.js +0 -15
- package/esm/RootModel/FormatDetails.d.ts +0 -24
- package/esm/RootModel/FormatDetails.js +0 -27
- package/esm/Session/DialogQueue.d.ts +0 -17
- package/esm/Session/DialogQueue.js +0 -31
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { maybePluginUrl } from '@jbrowse/core/pluginDefinitions';
|
|
2
|
+
import { SHARE_PREFIX, diffTrackConfig, flattenTrackConfigDelta, } from '@jbrowse/core/util';
|
|
3
|
+
import { getChildType, getPropertyMembers, getSnapshot, isArrayType, isMapType, isModelType, isReferenceType, isValidReference, } from '@jbrowse/mobx-state-tree';
|
|
4
|
+
import { asArray, isRecord } from './snapshotUtils.js';
|
|
5
|
+
// Cleans a freshly-loaded session in place by dropping any array/map element
|
|
6
|
+
// that can't be hydrated — e.g. an open track whose `configuration` reference
|
|
7
|
+
// resolves to a dangling id or a structurally-invalid config and throws when
|
|
8
|
+
// read. Dropping keeps the invariant that the open set only ever holds usable
|
|
9
|
+
// tracks (matching the open/add paths, which refuse invalid configs), so
|
|
10
|
+
// downstream code never has to defend against a track whose config access
|
|
11
|
+
// throws. Returns the dropped nodes so the caller can tell the user which
|
|
12
|
+
// tracks went missing.
|
|
13
|
+
//
|
|
14
|
+
// Dangling references in collections are NOT this function's job: every session
|
|
15
|
+
// reference is a `types.safeReference`, whose onInvalidated removes the entry
|
|
16
|
+
// from its parent at load. The `isValidReference` branch below is a backstop
|
|
17
|
+
// for any future plain `types.reference` collection.
|
|
18
|
+
export function filterSessionInPlace(node, type, dropped = []) {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
20
|
+
if (node !== undefined) {
|
|
21
|
+
if (isArrayType(type)) {
|
|
22
|
+
const array = node;
|
|
23
|
+
const childType = getChildType(node);
|
|
24
|
+
const isRef = isReferenceType(childType);
|
|
25
|
+
for (let i = 0; i < array.length;) {
|
|
26
|
+
if (!walkChildOrDrop(() => array[i], childType, isRef, dropped)) {
|
|
27
|
+
array.splice(i, 1);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
i += 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (isMapType(type)) {
|
|
35
|
+
const map = node;
|
|
36
|
+
const childType = getChildType(map);
|
|
37
|
+
const isRef = isReferenceType(childType);
|
|
38
|
+
for (const key of map.keys()) {
|
|
39
|
+
if (!walkChildOrDrop(() => map.get(key), childType, isRef, dropped)) {
|
|
40
|
+
map.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (isModelType(type)) {
|
|
45
|
+
const { properties } = getPropertyMembers(node);
|
|
46
|
+
// A node holding a `configuration` reference is a track/display/
|
|
47
|
+
// connection. Its only load-time integrity concern is that the config
|
|
48
|
+
// resolves: reading it throws for a dangling id or a structurally-invalid
|
|
49
|
+
// config, which the caller turns into a drop. Stop here instead of
|
|
50
|
+
// recursing — the first config-bearing node on the path to a display is
|
|
51
|
+
// the track itself, and recursing past it would eagerly instantiate child
|
|
52
|
+
// state models (displays) and run their afterAttach before the view is
|
|
53
|
+
// measured. That throws on view.width and would be misread as an invalid
|
|
54
|
+
// node, dropping a valid track. Display configs resolve through their own
|
|
55
|
+
// safety net (the track config's preProcessSnapshot injects a stub
|
|
56
|
+
// display per registered type).
|
|
57
|
+
if ('configuration' in properties) {
|
|
58
|
+
void node.configuration;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
for (const [pname, ptype] of Object.entries(properties)) {
|
|
62
|
+
filterSessionInPlace(node[pname], ptype, dropped);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return dropped;
|
|
68
|
+
}
|
|
69
|
+
// Returns false if the collection element should be dropped: a dangling
|
|
70
|
+
// reference (removed silently — see header), or a config-bearing node that
|
|
71
|
+
// throws while hydrating (recorded in `dropped` so the caller can report it).
|
|
72
|
+
// Otherwise recurses into it and returns true.
|
|
73
|
+
function walkChildOrDrop(get, childType, isRef, dropped) {
|
|
74
|
+
if (isRef) {
|
|
75
|
+
return isValidReference(get);
|
|
76
|
+
}
|
|
77
|
+
let child;
|
|
78
|
+
try {
|
|
79
|
+
child = get();
|
|
80
|
+
filterSessionInPlace(child, childType, dropped);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
console.error(e);
|
|
85
|
+
dropped.push(describeDroppedNode(child));
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Identifies a dropped node for a user-facing message by reading its serialized
|
|
90
|
+
// snapshot, which keeps a dangling `configuration` reference (the usual cause of
|
|
91
|
+
// a drop) from throwing again — references serialize to their stored id string.
|
|
92
|
+
function describeDroppedNode(node) {
|
|
93
|
+
try {
|
|
94
|
+
if (node !== undefined) {
|
|
95
|
+
const snap = getSnapshot(node);
|
|
96
|
+
return {
|
|
97
|
+
type: typeof snap.type === 'string' ? snap.type : undefined,
|
|
98
|
+
configuration: typeof snap.configuration === 'string'
|
|
99
|
+
? snap.configuration
|
|
100
|
+
: undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
console.error(e);
|
|
106
|
+
}
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
function locationDisplayName(loc) {
|
|
110
|
+
switch (loc.locationType) {
|
|
111
|
+
case 'LocalPathLocation':
|
|
112
|
+
return typeof loc.localPath === 'string' ? loc.localPath : 'local file';
|
|
113
|
+
case 'BlobLocation':
|
|
114
|
+
case 'FileHandleLocation':
|
|
115
|
+
return typeof loc.name === 'string' ? loc.name : 'local file';
|
|
116
|
+
case 'UriLocation':
|
|
117
|
+
return typeof loc.uri === 'string' ? loc.uri : 'remote file';
|
|
118
|
+
default:
|
|
119
|
+
return 'unknown location';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function walkLocations(node, track, out) {
|
|
123
|
+
if (Array.isArray(node)) {
|
|
124
|
+
for (const item of node) {
|
|
125
|
+
walkLocations(item, track, out);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (typeof node === 'object' && node !== null) {
|
|
129
|
+
const obj = node;
|
|
130
|
+
const nextTrack = typeof obj.trackId === 'string'
|
|
131
|
+
? {
|
|
132
|
+
trackId: obj.trackId,
|
|
133
|
+
trackName: typeof obj.name === 'string' ? obj.name : undefined,
|
|
134
|
+
}
|
|
135
|
+
: track;
|
|
136
|
+
if (typeof obj.locationType === 'string' &&
|
|
137
|
+
obj.locationType !== 'UriLocation') {
|
|
138
|
+
out.push({
|
|
139
|
+
locationType: obj.locationType,
|
|
140
|
+
name: locationDisplayName(obj),
|
|
141
|
+
trackId: nextTrack?.trackId,
|
|
142
|
+
trackName: nextTrack?.trackName,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (const key of Object.keys(obj)) {
|
|
146
|
+
walkLocations(obj[key], nextTrack, out);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Walks a session/config snapshot and reports every file location that won't
|
|
151
|
+
// open on jbrowse-web. Anything that isn't a UriLocation (local paths from
|
|
152
|
+
// desktop, or blob/file-handle locations bound to another browser session) is
|
|
153
|
+
// non-portable; a fully-UriLocation snapshot is portable and can be handed to
|
|
154
|
+
// the web app directly.
|
|
155
|
+
export function analyzeWebPortability(snapshot) {
|
|
156
|
+
const nonPortable = [];
|
|
157
|
+
walkLocations(snapshot, undefined, nonPortable);
|
|
158
|
+
return { nonPortable, portable: nonPortable.length === 0 };
|
|
159
|
+
}
|
|
160
|
+
// Distinct file display names of a set of non-portable locations, in first-seen
|
|
161
|
+
// order.
|
|
162
|
+
function distinctNames(locations) {
|
|
163
|
+
return [...new Set(locations.map(l => l.name))];
|
|
164
|
+
}
|
|
165
|
+
// Reads a `trackId` off a loosely-typed session-track snapshot, or undefined.
|
|
166
|
+
function readTrackId(track) {
|
|
167
|
+
const id = isRecord(track) ? track.trackId : undefined;
|
|
168
|
+
return typeof id === 'string' ? id : undefined;
|
|
169
|
+
}
|
|
170
|
+
// Concatenate session-track lists, keeping the last entry per trackId so a track
|
|
171
|
+
// carried by both the prior session and the current snapshot ships once (a
|
|
172
|
+
// duplicate trackId is an MST identifier collision on load). Entries without a
|
|
173
|
+
// readable trackId are kept as-is, in order.
|
|
174
|
+
function concatTracksByTrackId(...lists) {
|
|
175
|
+
const byId = new Map();
|
|
176
|
+
const out = [];
|
|
177
|
+
for (const t of lists.flat()) {
|
|
178
|
+
const id = readTrackId(t);
|
|
179
|
+
if (id === undefined) {
|
|
180
|
+
out.push(t);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
byId.set(id, t);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return [...out, ...byId.values()];
|
|
187
|
+
}
|
|
188
|
+
function readAssemblyName(assembly) {
|
|
189
|
+
const name = isRecord(assembly) ? assembly.name : undefined;
|
|
190
|
+
return typeof name === 'string' ? name : undefined;
|
|
191
|
+
}
|
|
192
|
+
// Same idea as concatTracksByTrackId for assemblies, whose identifier is their
|
|
193
|
+
// `name`. A duplicate is worse here than for tracks: it doesn't fail at load,
|
|
194
|
+
// it makes every assembly's `configuration` safeReference ambiguous, so the
|
|
195
|
+
// assemblyManager throws on the next read of one and the session is dead.
|
|
196
|
+
function concatAssembliesByName(...lists) {
|
|
197
|
+
const byName = new Map();
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const a of lists.flat()) {
|
|
200
|
+
const name = readAssemblyName(a);
|
|
201
|
+
if (name === undefined) {
|
|
202
|
+
out.push(a);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
byName.set(name, a);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return [...out, ...byName.values()];
|
|
209
|
+
}
|
|
210
|
+
// Session assemblies that don't collide with one the recipient's config already
|
|
211
|
+
// provides. `sessionAssemblies` and `jbrowse.assemblies` are concatenated into
|
|
212
|
+
// one namespace on the far side (`AssembliesMixin.assemblies`), and the
|
|
213
|
+
// duplicate-name guard there lives in `addSessionAssembly` — an *add* path,
|
|
214
|
+
// which a deserialized snapshot never goes through. So a name the hosted base
|
|
215
|
+
// also uses is not rejected on load, it silently makes every assembly's
|
|
216
|
+
// `configuration` safeReference ambiguous and takes the session down on the
|
|
217
|
+
// next read. The base's copy wins because it is the one `?config=` guarantees
|
|
218
|
+
// is there.
|
|
219
|
+
//
|
|
220
|
+
// Only the hostedConfigBase strategy needs this. A self-contained export ships
|
|
221
|
+
// no config at all, and concatAssembliesByName already keeps its own two lists
|
|
222
|
+
// from colliding with each other.
|
|
223
|
+
function withoutBaseAssemblies(sessionAssemblies, baseAssemblyNames) {
|
|
224
|
+
return sessionAssemblies.filter(a => {
|
|
225
|
+
const name = readAssemblyName(a);
|
|
226
|
+
return !name || !baseAssemblyNames.has(name);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// The assemblies a hosted-base export silently hands back to the hosted config.
|
|
230
|
+
// `coveredByBase` matches on name alone, and nothing ships an assembly edit: a
|
|
231
|
+
// user who attached a refNameAlias or cytoband file to a hub assembly exports a
|
|
232
|
+
// session that quietly uses the hub's original. Both halves of the loss are the
|
|
233
|
+
// same fact to a sender, so they are reported as one list — a config assembly
|
|
234
|
+
// whose config differs from the base's, and a session assembly that
|
|
235
|
+
// `withoutBaseAssemblies` drops to keep the recipient's safeReferences
|
|
236
|
+
// unambiguous.
|
|
237
|
+
//
|
|
238
|
+
// Edited-ness is `flattenTrackConfigDelta`, the same gate `splitTracksAgainstBase`
|
|
239
|
+
// uses, so an export can't call an assembly edited on a diff too thin to be worth
|
|
240
|
+
// shipping for a track.
|
|
241
|
+
function assembliesRevertedToBase(assemblies, sessionAssemblies, baseAssemblies) {
|
|
242
|
+
const baseByName = new Map(baseAssemblies.map(a => [a.name, a]));
|
|
243
|
+
const edited = assemblies.flatMap(assembly => {
|
|
244
|
+
const base = baseByName.get(assembly.name);
|
|
245
|
+
return base &&
|
|
246
|
+
flattenTrackConfigDelta(base, diffTrackConfig(base, assembly)).length > 0
|
|
247
|
+
? [assembly.name]
|
|
248
|
+
: [];
|
|
249
|
+
});
|
|
250
|
+
const shadowed = sessionAssemblies.flatMap(a => {
|
|
251
|
+
const name = readAssemblyName(a);
|
|
252
|
+
return name && baseByName.has(name) ? [name] : [];
|
|
253
|
+
});
|
|
254
|
+
return [...new Set([...edited, ...shadowed])];
|
|
255
|
+
}
|
|
256
|
+
// The `sessionAssemblies` override for a hosted-base session, or nothing at all
|
|
257
|
+
// when the session had none — the spread of `defaultSession` is what carries
|
|
258
|
+
// them otherwise, so an unconditional key would add an empty array to every
|
|
259
|
+
// export that never had one.
|
|
260
|
+
function hostedSessionAssemblies(prior, baseAssemblyNames) {
|
|
261
|
+
return prior.length > 0
|
|
262
|
+
? { sessionAssemblies: withoutBaseAssemblies(prior, baseAssemblyNames) }
|
|
263
|
+
: {};
|
|
264
|
+
}
|
|
265
|
+
// The trackIds the export is free to drop: the ones that name an entry in a
|
|
266
|
+
// track list it actually assembles. Everything else a location can be tagged
|
|
267
|
+
// with is structural and stays — an assembly's own sequence config carries
|
|
268
|
+
// `<name>-ReferenceSequenceTrack`, and an open connection track's config lives
|
|
269
|
+
// in `connectionTrackConfigs`, which rides along inside the session snapshot.
|
|
270
|
+
function droppableTrackIds(tracks, sessionTracks) {
|
|
271
|
+
return new Set([...tracks, ...sessionTracks].flatMap(t => readTrackId(t) ?? []));
|
|
272
|
+
}
|
|
273
|
+
// Drops a track's `textSearching` when its index is local — the desktop indexer
|
|
274
|
+
// writes the Trix ix/ixx/meta triple as LocalPathLocations
|
|
275
|
+
// (`addTrackTextSearchConf`), so indexing a remote BAM on desktop is enough to
|
|
276
|
+
// make an otherwise perfectly portable track non-portable and get it dropped
|
|
277
|
+
// whole. The data file is the track; the index is a search box over it. Shedding
|
|
278
|
+
// the index costs the recipient that box and nothing else, and under a hosted
|
|
279
|
+
// base the omission also lets the base's own (remote) index resolve, since a
|
|
280
|
+
// delta records adds and changes but never a deletion.
|
|
281
|
+
function withoutLocalTextSearch(track) {
|
|
282
|
+
if (!isRecord(track) ||
|
|
283
|
+
!isRecord(track.textSearching) ||
|
|
284
|
+
analyzeWebPortability(track.textSearching).portable) {
|
|
285
|
+
return track;
|
|
286
|
+
}
|
|
287
|
+
const { textSearching, ...rest } = track;
|
|
288
|
+
return rest;
|
|
289
|
+
}
|
|
290
|
+
// Display names of the tracks `withoutLocalTextSearch` stripped, for the report.
|
|
291
|
+
function strippedTextIndexNames(before, after) {
|
|
292
|
+
return [
|
|
293
|
+
...new Set(before.flatMap((track, i) => track !== after[i] && isRecord(track)
|
|
294
|
+
? [
|
|
295
|
+
typeof track.name === 'string'
|
|
296
|
+
? track.name
|
|
297
|
+
: (readTrackId(track) ?? 'unknown track'),
|
|
298
|
+
]
|
|
299
|
+
: [])),
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
// The tracks that reference a local file and can be shed by dropping them:
|
|
303
|
+
// their trackIds, to filter the shipped track lists with, and display names, to
|
|
304
|
+
// report to the user. A local file that no droppable track owns can't be shed
|
|
305
|
+
// this way and surfaces as a blockingFile instead.
|
|
306
|
+
function nonPortableUserTracks(report, droppable) {
|
|
307
|
+
const locs = report.nonPortable.flatMap(l => l.trackId && droppable.has(l.trackId)
|
|
308
|
+
? [{ trackId: l.trackId, name: l.trackName ?? l.trackId }]
|
|
309
|
+
: []);
|
|
310
|
+
return {
|
|
311
|
+
ids: new Set(locs.map(l => l.trackId)),
|
|
312
|
+
names: [...new Set(locs.map(l => l.name))],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
// Splits the portable tracks against the hosted base. A track with no base entry
|
|
316
|
+
// is user-added and ships whole in `addedTracks` (→ sessionTracks). A track that
|
|
317
|
+
// matches a base entry but differs from it becomes an `editDeltas` entry — the
|
|
318
|
+
// same channel the web session uses — so the recipient's base is overlaid with
|
|
319
|
+
// what the sender has. A track equal to its base entry produces neither and
|
|
320
|
+
// resolves from the base.
|
|
321
|
+
//
|
|
322
|
+
// "Differs" is not the same claim as "the user edited it", and the difference is
|
|
323
|
+
// worth naming. The oracle here is the hub as it is NOW, while desktop's copy is
|
|
324
|
+
// the hub as it was when the session was created (desktop edits jbrowse.tracks in
|
|
325
|
+
// place, keeping the base trackId, and persists the whole config). So a track the
|
|
326
|
+
// hub itself changed since then also lands in `editDeltas`, pinning the recipient
|
|
327
|
+
// to the values the sender is looking at. That is the export's stated intent —
|
|
328
|
+
// reproduce the sender's screen, the same reason `bakeSessionCascades` exists —
|
|
329
|
+
// but the delta channel it travels in means "user override" on the far side, and
|
|
330
|
+
// a delta masks later admin changes to that track. Telling the two apart needs
|
|
331
|
+
// the base as it was at session creation, which nothing persists.
|
|
332
|
+
function splitTracksAgainstBase(tracks, baseTracks) {
|
|
333
|
+
const baseById = new Map(baseTracks.map(t => [t.trackId, t]));
|
|
334
|
+
const addedTracks = tracks.filter(t => !baseById.has(t.trackId));
|
|
335
|
+
const editDeltas = Object.fromEntries(tracks.flatMap((track) => {
|
|
336
|
+
const base = baseById.get(track.trackId);
|
|
337
|
+
if (!base) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
const delta = diffTrackConfig(base, track);
|
|
341
|
+
// gate on real slot changes, not the identity keys / injected display
|
|
342
|
+
// stubs a raw diff carries (matches the web session's "is edited" test)
|
|
343
|
+
return flattenTrackConfigDelta(base, delta).length > 0
|
|
344
|
+
? [[track.trackId, delta]]
|
|
345
|
+
: [];
|
|
346
|
+
}));
|
|
347
|
+
return { addedTracks, editDeltas };
|
|
348
|
+
}
|
|
349
|
+
// Overlays edited-track deltas onto the session's trackConfigDeltas, preserving
|
|
350
|
+
// any the base session already carried. A no-op (returns the session unchanged)
|
|
351
|
+
// when there are no edits, so the exported snapshot stays minimal.
|
|
352
|
+
function withDeltas(session, editDeltas) {
|
|
353
|
+
if (Object.keys(editDeltas).length === 0) {
|
|
354
|
+
return session;
|
|
355
|
+
}
|
|
356
|
+
const prior = isRecord(session.trackConfigDeltas)
|
|
357
|
+
? session.trackConfigDeltas
|
|
358
|
+
: {};
|
|
359
|
+
return { ...session, trackConfigDeltas: { ...prior, ...editDeltas } };
|
|
360
|
+
}
|
|
361
|
+
// The plugin definitions the recipient will not already have, carried in the
|
|
362
|
+
// session as `sessionPlugins` — the one channel a jbrowse-web session has for
|
|
363
|
+
// them, and one it triages before loading (SessionLoader.loadSession). Without
|
|
364
|
+
// this, a desktop session using a plugin's view or track type opens on a
|
|
365
|
+
// jbrowse-web that never registered the type, and the view is dropped on load.
|
|
366
|
+
//
|
|
367
|
+
// Under `hostedConfigBase` jbrowse-web loads the base config's own `plugins[]`,
|
|
368
|
+
// so only the extras ride along; a `selfContained` export opens `?config=none`
|
|
369
|
+
// and has no config to load any from, so it carries them all. Identity is the
|
|
370
|
+
// url a definition loads from (`maybePluginUrl`) rather than its name, since an
|
|
371
|
+
// ESM/CJS definition need not have one; a definition naming no loader at all is
|
|
372
|
+
// never "the same plugin" as another, so it is always carried.
|
|
373
|
+
function extraPlugins(plugins, basePlugins) {
|
|
374
|
+
const baseUrls = new Set(basePlugins.flatMap(p => maybePluginUrl(p) ?? []));
|
|
375
|
+
return plugins.filter(p => {
|
|
376
|
+
const url = maybePluginUrl(p);
|
|
377
|
+
return !url || !baseUrls.has(url);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// Connections the recipient will not already have, carried the same way and for
|
|
381
|
+
// the same reason as the plugins above: desktop keeps them in `jbrowse
|
|
382
|
+
// .connections`, which nothing in the export ships, so without this a session
|
|
383
|
+
// with a track hub arrives on the web with the hub gone. Only the connection
|
|
384
|
+
// tracks the sender had open survive on their own, through the session's
|
|
385
|
+
// `connectionTrackConfigs`; the hub itself — and every track in it the recipient
|
|
386
|
+
// might want to open next — needs `sessionConnections`, jbrowse-web's
|
|
387
|
+
// session-level counterpart of `jbrowse.connections`.
|
|
388
|
+
//
|
|
389
|
+
// Identity is `connectionId`, which is what the recipient's own
|
|
390
|
+
// `connections` getter concatenates on, so shipping one the base already
|
|
391
|
+
// declares would list the same hub twice.
|
|
392
|
+
function extraConnections(connections, baseConnections) {
|
|
393
|
+
const baseIds = new Set(baseConnections.flatMap(c => readId(c) ?? []));
|
|
394
|
+
return connections.filter(c => {
|
|
395
|
+
const id = readId(c);
|
|
396
|
+
return !id || !baseIds.has(id);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function readId(connection) {
|
|
400
|
+
const id = isRecord(connection) ? connection.connectionId : undefined;
|
|
401
|
+
return typeof id === 'string' ? id : undefined;
|
|
402
|
+
}
|
|
403
|
+
// Collects every `internetAccountId` a snapshot's file locations name, in
|
|
404
|
+
// first-seen order.
|
|
405
|
+
function referencedAccountIds(node, out) {
|
|
406
|
+
if (Array.isArray(node)) {
|
|
407
|
+
for (const item of node) {
|
|
408
|
+
referencedAccountIds(item, out);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (isRecord(node)) {
|
|
412
|
+
if (typeof node.internetAccountId === 'string') {
|
|
413
|
+
out.add(node.internetAccountId);
|
|
414
|
+
}
|
|
415
|
+
for (const value of Object.values(node)) {
|
|
416
|
+
referencedAccountIds(value, out);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
// The accounts a shipped file names that the recipient has no way to build.
|
|
422
|
+
// Internet accounts live in the root config alone — there is no
|
|
423
|
+
// `sessionInternetAccounts` for one to ride in — so a hosted-base export carries
|
|
424
|
+
// exactly the accounts the base config declares, and a self-contained export
|
|
425
|
+
// (`?config=none`) carries none at all. The recipient does not crash on a
|
|
426
|
+
// missing one (`findAppropriateInternetAccount` returns null rather than pushing
|
|
427
|
+
// an unknown type into the union), it fetches the file unauthenticated and
|
|
428
|
+
// reports the 401, which is worth the sender knowing before they send the link.
|
|
429
|
+
function unavailableAccountIds(session, baseAccounts) {
|
|
430
|
+
const available = new Set(baseAccounts.flatMap(a => a.internetAccountId ?? []));
|
|
431
|
+
return [...referencedAccountIds(session, new Set())].filter(id => !available.has(id));
|
|
432
|
+
}
|
|
433
|
+
// Adds the session-level carriers to the exported session, leaving each key out
|
|
434
|
+
// entirely when it would be empty so the exported snapshot stays minimal (same
|
|
435
|
+
// rule as withDeltas above).
|
|
436
|
+
function withCarriedConfigs(session, plugins, connections) {
|
|
437
|
+
return {
|
|
438
|
+
...session,
|
|
439
|
+
...(plugins.length > 0 ? { sessionPlugins: plugins } : {}),
|
|
440
|
+
...(connections.length > 0 ? { sessionConnections: connections } : {}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
// Decides how to hand a desktop session to jbrowse-web. When the session was
|
|
444
|
+
// launched from a hosted hub config (sourceConfigUrl) that still covers all of
|
|
445
|
+
// its assemblies, that config is reused as the base and only user-added/edited
|
|
446
|
+
// tracks ride along; otherwise the session is made self-contained. `baseConfig`
|
|
447
|
+
// is the fetched hub config (already rebased with addRelativeUris by the caller),
|
|
448
|
+
// used to tell hub tracks from user-added ones and to diff edited hub tracks.
|
|
449
|
+
export function planWebExport(snapshot, baseConfig) {
|
|
450
|
+
const sourceConfigUrl = snapshot.configuration?.sourceConfigUrl;
|
|
451
|
+
const assemblies = snapshot.assemblies ?? [];
|
|
452
|
+
const defaultSession = snapshot.defaultSession ?? {};
|
|
453
|
+
const priorSessionAssemblies = asArray(defaultSession.sessionAssemblies);
|
|
454
|
+
const inputTracks = snapshot.tracks ?? [];
|
|
455
|
+
const inputSessionTracks = asArray(defaultSession.sessionTracks);
|
|
456
|
+
const allTracks = inputTracks.map(withoutLocalTextSearch);
|
|
457
|
+
const allSessionTracks = inputSessionTracks.map(withoutLocalTextSearch);
|
|
458
|
+
// over the stripped lists, so a track whose only local file was its own search
|
|
459
|
+
// index is no longer read as one that has to be dropped
|
|
460
|
+
const report = analyzeWebPortability({
|
|
461
|
+
...snapshot,
|
|
462
|
+
tracks: allTracks,
|
|
463
|
+
defaultSession: { ...defaultSession, sessionTracks: allSessionTracks },
|
|
464
|
+
});
|
|
465
|
+
const dropped = nonPortableUserTracks(report, droppableTrackIds(allTracks, allSessionTracks));
|
|
466
|
+
const keep = (t) => {
|
|
467
|
+
const id = readTrackId(t);
|
|
468
|
+
return !id || !dropped.ids.has(id);
|
|
469
|
+
};
|
|
470
|
+
const tracks = allTracks.filter(keep);
|
|
471
|
+
const priorSessionTracks = allSessionTracks.filter(keep);
|
|
472
|
+
const baseAssemblyNames = new Set((baseConfig?.assemblies ?? []).map(a => a.name));
|
|
473
|
+
const coveredByBase = !!sourceConfigUrl &&
|
|
474
|
+
!!baseConfig &&
|
|
475
|
+
assemblies.every(a => baseAssemblyNames.has(a.name));
|
|
476
|
+
// `baseConfig` is undefined only because the caller could not fetch
|
|
477
|
+
// `sourceConfigUrl` — it is the same fetch that produces it — so the two ways
|
|
478
|
+
// a hub drops out of the plan are distinguishable here without a flag.
|
|
479
|
+
const selfContainedReason = coveredByBase
|
|
480
|
+
? undefined
|
|
481
|
+
: !sourceConfigUrl
|
|
482
|
+
? 'noSourceConfig'
|
|
483
|
+
: baseConfig
|
|
484
|
+
? 'assembliesNotInBase'
|
|
485
|
+
: 'baseUnreachable';
|
|
486
|
+
const plugins = snapshot.plugins ?? [];
|
|
487
|
+
const connections = snapshot.connections ?? [];
|
|
488
|
+
// defined exactly when the hostedConfigBase strategy applies, so it doubles as
|
|
489
|
+
// the strategy flag for the shared tail below
|
|
490
|
+
const hosted = coveredByBase
|
|
491
|
+
? splitTracksAgainstBase(tracks, baseConfig.tracks ?? [])
|
|
492
|
+
: undefined;
|
|
493
|
+
const session = withCarriedConfigs(hosted
|
|
494
|
+
? withDeltas({
|
|
495
|
+
...defaultSession,
|
|
496
|
+
...hostedSessionAssemblies(priorSessionAssemblies, baseAssemblyNames),
|
|
497
|
+
sessionTracks: concatTracksByTrackId(priorSessionTracks, hosted.addedTracks),
|
|
498
|
+
}, hosted.editDeltas)
|
|
499
|
+
: {
|
|
500
|
+
...defaultSession,
|
|
501
|
+
sessionAssemblies: concatAssembliesByName(priorSessionAssemblies, assemblies),
|
|
502
|
+
sessionTracks: concatTracksByTrackId(priorSessionTracks, tracks),
|
|
503
|
+
}, hosted ? extraPlugins(plugins, baseConfig?.plugins ?? []) : plugins, hosted
|
|
504
|
+
? extraConnections(connections, asArray(baseConfig?.connections))
|
|
505
|
+
: connections);
|
|
506
|
+
return {
|
|
507
|
+
strategy: hosted ? 'hostedConfigBase' : 'selfContained',
|
|
508
|
+
configUrl: hosted ? sourceConfigUrl : undefined,
|
|
509
|
+
webBaseUrl: resolveWebBaseUrl(snapshot.configuration?.webExportUrl),
|
|
510
|
+
session,
|
|
511
|
+
droppedTracks: dropped.names,
|
|
512
|
+
// Re-run detection over the session that will actually ship rather than
|
|
513
|
+
// filtering the input report by trackId. The two are not the same set, in
|
|
514
|
+
// both directions: a hosted-base export leaves the config's assemblies
|
|
515
|
+
// behind (their local files are the base's problem, not the recipient's)
|
|
516
|
+
// but still ships the prior session's `sessionAssemblies`, and either
|
|
517
|
+
// strategy ships `connectionTrackConfigs` that the droppable-track filter
|
|
518
|
+
// above never touches. Anything non-portable still present here survived
|
|
519
|
+
// the drop and so blocks the session by definition.
|
|
520
|
+
blockingFiles: distinctNames(analyzeWebPortability(session).nonPortable),
|
|
521
|
+
selfContainedReason,
|
|
522
|
+
droppedTextIndexes: [
|
|
523
|
+
...new Set([
|
|
524
|
+
...strippedTextIndexNames(inputTracks, allTracks),
|
|
525
|
+
...strippedTextIndexNames(inputSessionTracks, allSessionTracks),
|
|
526
|
+
]),
|
|
527
|
+
],
|
|
528
|
+
revertedAssemblies: hosted
|
|
529
|
+
? assembliesRevertedToBase(assemblies, priorSessionAssemblies, baseConfig?.assemblies ?? [])
|
|
530
|
+
: [],
|
|
531
|
+
unavailableAccounts: unavailableAccountIds(session, hosted ? (baseConfig?.internetAccounts ?? []) : []),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// The deployed jbrowse-web the desktop "export to web" action targets. Stable
|
|
535
|
+
// per the maintainers; a future target will be added alongside, not replace it.
|
|
536
|
+
export const DEFAULT_WEB_BASE_URL = 'https://jbrowse.org/code/jb2/latest/';
|
|
537
|
+
// Where an export opens. A config can point its users at a different jbrowse-web
|
|
538
|
+
// through the desktop-only `webExportUrl` slot — a site that runs its own
|
|
539
|
+
// deployment, and whose data may only be reachable from it.
|
|
540
|
+
//
|
|
541
|
+
// A value that isn't an absolute http(s) url falls back rather than throwing:
|
|
542
|
+
// this runs inside the export, and a typo in a config slot must not be able to
|
|
543
|
+
// make exporting impossible. Same posture as the base-config fetch, which logs
|
|
544
|
+
// and falls back to a self-contained export.
|
|
545
|
+
function resolveWebBaseUrl(webExportUrl) {
|
|
546
|
+
if (!webExportUrl) {
|
|
547
|
+
return DEFAULT_WEB_BASE_URL;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const url = new URL(webExportUrl);
|
|
551
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
552
|
+
throw new Error(`not an http(s) url: ${webExportUrl}`);
|
|
553
|
+
}
|
|
554
|
+
// The slot names a deployment, so whatever params the value came with are
|
|
555
|
+
// not part of it — and the easiest way to fill the slot is to paste a
|
|
556
|
+
// jbrowse-web address out of the browser, which carries the pasting user's
|
|
557
|
+
// own `#config=…&session=…`. Params live in the hash XOR the query string
|
|
558
|
+
// and the hash wins whenever it holds any (queryParams.ts), so a leftover
|
|
559
|
+
// hash silently opens that session instead of the exported one.
|
|
560
|
+
url.search = '';
|
|
561
|
+
url.hash = '';
|
|
562
|
+
return url.href;
|
|
563
|
+
}
|
|
564
|
+
catch (e) {
|
|
565
|
+
console.error(e);
|
|
566
|
+
return DEFAULT_WEB_BASE_URL;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Assembles the jbrowse-web URL for an export plan. `sessionParam` is the
|
|
570
|
+
// ready-made `session` value: `share-<id>` for a short lambda link (pass its
|
|
571
|
+
// `password`) or `encoded-<b64>`/`json-<json>` for an inline long link.
|
|
572
|
+
// `config` points at the hosted base, or `none` for a self-contained session.
|
|
573
|
+
//
|
|
574
|
+
// `exportedFrom` stamps what produced the link. These links are artifacts —
|
|
575
|
+
// papers, supplements, emails — and every other thing they name moves under
|
|
576
|
+
// them: DEFAULT_WEB_BASE_URL is `.../latest/`, so the deployment that opens the
|
|
577
|
+
// session is an unknown future build, and the hosted base config the plan
|
|
578
|
+
// diffed against is fetched fresh on both ends and can be in a third state by
|
|
579
|
+
// then. Pinning either is a deployment decision; recording the producer is not,
|
|
580
|
+
// and it is the difference between a recipient who can say which JBrowse made
|
|
581
|
+
// this and one guessing. jbrowse-web reads a fixed list of params
|
|
582
|
+
// (createSessionLoader) and ignores the rest, so the stamp is inert there and
|
|
583
|
+
// stays in the address bar where a reader can see it.
|
|
584
|
+
//
|
|
585
|
+
// The large inline modes (`encoded-`/`json-`) go in the hash fragment, which is
|
|
586
|
+
// never sent to the server and so can't trip the request-line limit (HTTP 414)
|
|
587
|
+
// the query string can — a self-contained export carries its own assemblies and
|
|
588
|
+
// tracks and is exactly the biggest kind of session. The tiny `share-<id>` short
|
|
589
|
+
// link stays in the query string. Mirrors jbrowse-web's buildShareUrl; the
|
|
590
|
+
// SessionLoader reads `session=`/`config=` from either location (hash XOR query).
|
|
591
|
+
export function buildWebExportUrl(
|
|
592
|
+
// where the link points and what config it loads: the plan's report fields
|
|
593
|
+
// (what was dropped, what reverted) are for the sender's screen, not the URL
|
|
594
|
+
plan, sessionParam, options = {}) {
|
|
595
|
+
const url = new URL(plan.webBaseUrl);
|
|
596
|
+
const params = new URLSearchParams();
|
|
597
|
+
params.set('config', plan.configUrl ?? 'none');
|
|
598
|
+
params.set('session', sessionParam);
|
|
599
|
+
if (options.password) {
|
|
600
|
+
params.set('password', options.password);
|
|
601
|
+
}
|
|
602
|
+
if (options.exportedFrom) {
|
|
603
|
+
params.set('exportedFrom', options.exportedFrom);
|
|
604
|
+
}
|
|
605
|
+
const str = params.toString();
|
|
606
|
+
if (sessionParam.startsWith(SHARE_PREFIX)) {
|
|
607
|
+
url.search = str;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
url.hash = str;
|
|
611
|
+
}
|
|
612
|
+
return url.href;
|
|
613
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { BaseRootModel } from './RootModel/BaseRootModel.ts';
|
|
2
|
+
import type { BaseSession } from './Session/BaseSession.ts';
|
|
3
|
+
/**
|
|
4
|
+
* `types.compose` can't thread the types of sibling mixins into one another, so
|
|
5
|
+
* a mixin that reaches members contributed by a co-composed base (`jbrowse`,
|
|
6
|
+
* `adminMode`, `session`, ...) must assert that base onto its own partial
|
|
7
|
+
* `self`. These helpers name that assertion in one place — instead of the
|
|
8
|
+
* ad-hoc inline `s as typeof s & BaseSession` scattered across mixins — so the
|
|
9
|
+
* assert target is fixed (can't drift to the wrong type) and the seam is
|
|
10
|
+
* greppable. The cast is unavoidable: it encodes the compose-time invariant
|
|
11
|
+
* that these mixins are only ever composed onto the named base.
|
|
12
|
+
*/
|
|
13
|
+
export declare function asSession<S>(self: S): S & BaseSession;
|
|
14
|
+
export declare function asRoot<S>(self: S): S & BaseRootModel;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `types.compose` can't thread the types of sibling mixins into one another, so
|
|
3
|
+
* a mixin that reaches members contributed by a co-composed base (`jbrowse`,
|
|
4
|
+
* `adminMode`, `session`, ...) must assert that base onto its own partial
|
|
5
|
+
* `self`. These helpers name that assertion in one place — instead of the
|
|
6
|
+
* ad-hoc inline `s as typeof s & BaseSession` scattered across mixins — so the
|
|
7
|
+
* assert target is fixed (can't drift to the wrong type) and the seam is
|
|
8
|
+
* greppable. The cast is unavoidable: it encodes the compose-time invariant
|
|
9
|
+
* that these mixins are only ever composed onto the named base.
|
|
10
|
+
*/
|
|
11
|
+
export function asSession(self) {
|
|
12
|
+
return self;
|
|
13
|
+
}
|
|
14
|
+
export function asRoot(self) {
|
|
15
|
+
return self;
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two guards every loosely-typed snapshot walk in this package needs. A
|
|
3
|
+
* session or config snapshot arrives as `unknown` at these boundaries — it is
|
|
4
|
+
* `types.frozen` data, or a plain object read back off disk — so it is narrowed
|
|
5
|
+
* by hand rather than by MST.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
8
|
+
export declare function asArray(v: unknown): unknown[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two guards every loosely-typed snapshot walk in this package needs. A
|
|
3
|
+
* session or config snapshot arrives as `unknown` at these boundaries — it is
|
|
4
|
+
* `types.frozen` data, or a plain object read back off disk — so it is narrowed
|
|
5
|
+
* by hand rather than by MST.
|
|
6
|
+
*/
|
|
7
|
+
export function isRecord(v) {
|
|
8
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
9
|
+
}
|
|
10
|
+
export function asArray(v) {
|
|
11
|
+
return Array.isArray(v) ? v : [];
|
|
12
|
+
}
|