@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
347 lines (346 loc) • 11.7 kB
JavaScript
import { getParent, getSnapshot, isRoot, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
import { getFileHandle, storeFileHandle, verifyPermission, } from "./fileHandleStore.js";
import { getEnv, getSession, objectHash } from "./index.js";
import { readConfObject } from "../configuration/index.js";
const trackAssemblyNamesCache = new WeakMap();
export function getTrackAssemblyNames(track) {
let cached = trackAssemblyNamesCache.get(track);
if (!cached) {
cached = getConfAssemblyNames(track.configuration);
trackAssemblyNamesCache.set(track, cached);
}
return cached;
}
export function getConfAssemblyNames(conf) {
const trackAssemblyNames = readConfObject(conf, 'assemblyNames');
if (!trackAssemblyNames) {
const parent = getParent(conf);
if ('sequence' in parent) {
return [readConfObject(parent, 'name')];
}
else {
throw new Error('unknown assembly names');
}
}
return trackAssemblyNames;
}
export function getRpcSessionId(thisNode) {
let highestRpcSessionId;
for (let node = thisNode; !isRoot(node); node = getParent(node)) {
if ('rpcSessionId' in node) {
highestRpcSessionId = node.rpcSessionId;
}
}
if (!highestRpcSessionId) {
throw new Error('getRpcSessionId failed, no parent node in the state tree has an `rpcSessionId` attribute');
}
return highestRpcSessionId;
}
export function getParentRenderProps(node) {
for (let currentNode = getParent(node); !isRoot(currentNode); currentNode = getParent(currentNode)) {
if ('renderProps' in currentNode) {
return currentNode.renderProps();
}
}
return {};
}
export const UNKNOWN = 'UNKNOWN';
export const UNSUPPORTED = 'UNSUPPORTED';
let blobMap = {};
export function getBlob(id) {
return blobMap[id];
}
export function getBlobMap() {
return blobMap;
}
export function setBlobMap(map) {
blobMap = map;
}
let counter = 0;
export function storeBlobLocation(location) {
if ('blob' in location) {
const blobId = `b${Date.now()}-${counter++}`;
blobMap[blobId] = location.blob;
return {
name: location.blob.name,
blobId,
locationType: 'BlobLocation',
};
}
return location;
}
let fileHandleCache = {};
export function getFileFromCache(handleId) {
return fileHandleCache[handleId];
}
export function setFileInCache(handleId, file) {
fileHandleCache[handleId] = file;
}
export function clearFileFromCache(handleId) {
delete fileHandleCache[handleId];
}
export function getFileHandleCache() {
return fileHandleCache;
}
export function setFileHandleCache(cache) {
fileHandleCache = cache;
}
export async function ensureFileHandleReady(handleId, requestPermission = true) {
const cached = fileHandleCache[handleId];
if (cached) {
return cached;
}
const handle = await getFileHandle(handleId);
if (!handle) {
throw new Error(`File handle not found for handleId: ${handleId}. The file may have been opened in a different browser or the IndexedDB was cleared.`);
}
const hasPermission = await verifyPermission(handle, requestPermission);
if (!hasPermission) {
throw new Error(`Permission denied for file "${handle.name}". Click "Restore access" to grant permission.`);
}
const file = await handle.getFile();
fileHandleCache[handleId] = file;
return file;
}
export async function storeFileHandleLocation(handle) {
const handleId = await storeFileHandle(handle);
const file = await handle.getFile();
fileHandleCache[handleId] = file;
return {
locationType: 'FileHandleLocation',
name: handle.name,
handleId,
};
}
export async function restoreFileHandles(handleIds, requestPermission = false) {
const results = [];
for (const handleId of handleIds) {
try {
await ensureFileHandleReady(handleId, requestPermission);
results.push({ handleId, success: true });
}
catch (e) {
results.push({ handleId, success: false, error: e });
}
}
return results;
}
export function findFileHandleIds(obj, handleIds = new Set(), seen = new WeakSet()) {
if (!obj || typeof obj !== 'object') {
return handleIds;
}
if (seen.has(obj)) {
return handleIds;
}
seen.add(obj);
if (Array.isArray(obj)) {
for (const item of obj) {
findFileHandleIds(item, handleIds, seen);
}
}
else {
const record = obj;
if (record.locationType === 'FileHandleLocation' &&
typeof record.handleId === 'string') {
handleIds.add(record.handleId);
}
for (const value of Object.values(record)) {
findFileHandleIds(value, handleIds, seen);
}
}
return handleIds;
}
export async function restoreFileHandlesFromSnapshot(sessionSnapshot, requestPermission = false) {
const handleIds = findFileHandleIds(sessionSnapshot);
if (handleIds.size > 0) {
return restoreFileHandles([...handleIds], requestPermission);
}
return [];
}
let pendingFileHandleIds = [];
export function getPendingFileHandleIds() {
return pendingFileHandleIds;
}
export function setPendingFileHandleIds(ids) {
pendingFileHandleIds = ids;
}
export function clearPendingFileHandleIds() {
pendingFileHandleIds = [];
}
export async function restorePendingFileHandles() {
if (pendingFileHandleIds.length === 0) {
return [];
}
const results = await restoreFileHandles(pendingFileHandleIds, true);
const stillFailed = results.filter(r => !r.success).map(r => r.handleId);
pendingFileHandleIds = stillFailed;
return results;
}
export function makeIndex(location, suffix) {
if ('uri' in location) {
return {
uri: location.uri + suffix,
locationType: 'UriLocation',
};
}
else if ('localPath' in location) {
return {
localPath: location.localPath + suffix,
locationType: 'LocalPathLocation',
};
}
else {
return location;
}
}
export function makeIndexType(name, typeA, typeB) {
return name?.toUpperCase().endsWith(typeA) ? typeA : typeB;
}
export function getFileName(track) {
const uri = 'uri' in track ? track.uri : undefined;
const localPath = 'localPath' in track ? track.localPath : undefined;
const blob = 'blobId' in track ? track : undefined;
const fileHandle = 'handleId' in track ? track : undefined;
if (blob?.name) {
return blob.name;
}
if (fileHandle?.name) {
return fileHandle.name;
}
if (uri) {
const normalized = uri.replace(/\\/g, '/');
return normalized.slice(normalized.lastIndexOf('/') + 1);
}
if (localPath) {
const normalized = localPath.replace(/\\/g, '/');
return normalized.slice(normalized.lastIndexOf('/') + 1);
}
return '';
}
export function guessAdapter(file, index, adapterHint, model) {
if (model) {
const { pluginManager } = getEnv(model);
const adapterGuesser = pluginManager.evaluateExtensionPoint('Core-guessAdapterForLocation', (_file, _index, _adapterHint) => {
return undefined;
});
const adapter = adapterGuesser(file, index, adapterHint);
if (adapter) {
return adapter;
}
}
return {
type: UNKNOWN,
};
}
export function guessTrackType(adapterType, model, file) {
if (model) {
const session = getSession(model);
const trackTypeGuesser = getEnv(session).pluginManager.evaluateExtensionPoint('Core-guessTrackTypeForLocation', (_adapterName) => {
return undefined;
});
const trackType = trackTypeGuesser(adapterType, file);
if (trackType) {
return trackType;
}
}
return 'FeatureTrack';
}
export function generateUnsupportedTrackConf(trackName, trackUrl, categories) {
const conf = {
type: 'FeatureTrack',
name: `${trackName} (Unsupported)`,
description: `Support not yet implemented for "${trackUrl}"`,
category: categories,
trackId: '',
};
conf.trackId = objectHash(conf);
return conf;
}
export function generateUnknownTrackConf(trackName, trackUrl, categories) {
const conf = {
type: 'FeatureTrack',
name: `${trackName} (Unknown)`,
description: `Could not determine track type for "${trackUrl}"`,
category: categories,
trackId: '',
};
conf.trackId = objectHash(conf);
return conf;
}
export function getTrackName(conf, session) {
const trackName = isStateTreeNode(conf)
? readConfObject(conf, 'name')
: (conf.name ?? '');
const trackType = isStateTreeNode(conf)
? readConfObject(conf, 'type')
: conf.type;
if (!trackName && trackType === 'ReferenceSequenceTrack') {
const asm = session.assemblies.find(a => a.sequence === conf);
return asm
? `Reference sequence (${readConfObject(asm, 'displayName') || readConfObject(asm, 'name')})`
: 'Reference sequence';
}
return trackName;
}
export function showTrackGeneric(self, trackId, initialSnapshot = {}, displayInitialSnapshot = {}) {
const { pluginManager } = getEnv(self);
const session = getSession(self);
const found = self.tracks.find(t => t.configuration.trackId === trackId);
if (found) {
return found;
}
const rawConf = session.getTracksById()[trackId];
if (!rawConf) {
throw new Error(`Could not resolve identifier "${trackId}"`);
}
const confSnapshot = isStateTreeNode(rawConf)
? getSnapshot(rawConf)
: structuredClone(rawConf);
const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot);
const trackType = pluginManager.getTrackType(conf.type);
if (!trackType) {
throw new Error(`Unknown track type ${conf.type}`);
}
const viewType = pluginManager.getViewType(self.type);
const supportedDisplays = new Set(viewType.displayTypes.map(d => d.name));
const displays = conf.displays ?? [];
const displayConf = displays.find((d) => supportedDisplays.has(d.type));
const snapshotType = displayInitialSnapshot.type;
const defaultDisplayType = displayConf?.type ??
trackType.displayTypes.find(d => supportedDisplays.has(d.name))?.name;
const displayType = snapshotType ?? defaultDisplayType;
if (!displayType) {
throw new Error(`Could not find a compatible display for view type ${self.type}`);
}
const displayId = displayConf?.displayId ?? `${trackId}-${displayType}`;
const { type: _type, displayId: _displayId, ...displayConfState } = displayConf ?? {};
const track = trackType.stateModel.create({
...initialSnapshot,
type: conf.type,
configuration: trackId,
displays: [
{
type: displayType,
configuration: displayId,
...displayConfState,
...displayInitialSnapshot,
},
],
});
self.tracks.push(track);
return track;
}
export function hideTrackGeneric(self, trackId) {
const t = self.tracks.find(t => t.configuration.trackId === trackId);
if (t) {
self.tracks.remove(t);
return 1;
}
return 0;
}
export function toggleTrackGeneric(self, trackId) {
const hiddenCount = hideTrackGeneric(self, trackId);
if (!hiddenCount) {
showTrackGeneric(self, trackId);
}
}