Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/workers/root-file/src/lib/root-file.protocol.ts is written in an unsupported language. File is not indexed.

0001 /**
0002  * The ROOT file worker's message protocol and its request handling, kept apart
0003  * from the worker entry point so that the handling runs in unit tests without
0004  * jsroot.
0005  *
0006  * Files are opened under HANDLES: each client (the display's loads, the data
0007  * selector's picker) works on its own handle id, so opening a file for one
0008  * never replaces the file another one is converting.
0009  */
0010 
0011 import type { ContentEntry, FileContentProbe } from '@dexvis/firebird-core/loaders';
0012 import type { DexDocument, PodioModel } from '@dexvis/root2dex';
0013 import { DEFAULT_MAX_ENTRIES, parseEntryRanges, selectEntries } from '@dexvis/root2dex/entries';
0014 
0015 /** What to open: a file the user picked/dropped, or a URL. */
0016 export type RootFileSource =
0017   | { kind: 'file'; file: File }
0018   | { kind: 'url'; url: string };
0019 
0020 export interface RootFileProbeRequest {
0021   type: 'probe';
0022   requestId: string;
0023   source: RootFileSource;
0024 }
0025 
0026 export interface RootFileOpenRequest {
0027   type: 'open';
0028   requestId: string;
0029   /** The handle the file opens under; replaces what that handle had open. */
0030   handle: number;
0031   source: RootFileSource;
0032 }
0033 
0034 /** What a conversion converts, besides the entries. */
0035 export interface RootConvertOptions {
0036   /**
0037    * Collection groups to convert (names from the open response's
0038    * `collectionGroups`). Absent or empty means all groups.
0039    */
0040   collections?: string[];
0041   /**
0042    * EDM4hep sim hit collections kept out of MC-truth trajectories; they still
0043    * convert as hits. Absent or empty excludes nothing.
0044    */
0045   trajectoryExcludedCollections?: string[];
0046 }
0047 
0048 export interface RootFileConvertRequest extends RootConvertOptions {
0049   type: 'convert';
0050   requestId: string;
0051   /** The handle whose open file is converted. */
0052   handle: number;
0053   /**
0054    * Entry numbers as typed by the user: '1', '0,2,4-5'. Entries outside the
0055    * file are dropped with one warning in `RootFileConverted.warnings`; a
0056    * selection with no entry in the file, or more than DEFAULT_MAX_ENTRIES
0057    * requested entries, is rejected before anything is expanded (the pyrobird
0058    * convert endpoint applies the same rules).
0059    */
0060   entries: string;
0061 }
0062 
0063 export interface RootFileCloseRequest {
0064   type: 'close';
0065   requestId: string;
0066   /** The handle to release; other handles keep their files. */
0067   handle: number;
0068 }
0069 
0070 export type RootFileRequest =
0071   | RootFileProbeRequest
0072   | RootFileOpenRequest
0073   | RootFileConvertRequest
0074   | RootFileCloseRequest;
0075 
0076 export interface RootFileProbed {
0077   type: 'probed';
0078   requestId: string;
0079   probe: FileContentProbe;
0080 }
0081 
0082 export interface RootFileOpened {
0083   type: 'opened';
0084   requestId: string;
0085   sourceName: string;
0086   model: PodioModel;
0087   entryCount: number;
0088   /** The collection groups this model's conversion knows (checkbox choices). */
0089   collectionGroups: string[];
0090 }
0091 
0092 export interface RootFileConverted {
0093   type: 'converted';
0094   requestId: string;
0095   /** The converted entry numbers: the request clamped to the file. */
0096   entries: number[];
0097   dex: DexDocument;
0098   /** The entry selection warning, then the converter warnings, in order. */
0099   warnings: string[];
0100   /** [load-timing] How long the conversion took in the worker. */
0101   convertMs: number;
0102   /**
0103    * [load-timing] Wall-clock (Date.now(), shared with the main thread) right
0104    * before postMessage — the receiver's Date.now() minus this is the
0105    * serialize + queue + deserialize cost of shipping the document.
0106    */
0107   postedAtMs: number;
0108 }
0109 
0110 export interface RootFileClosed {
0111   type: 'closed';
0112   requestId: string;
0113 }
0114 
0115 export interface RootFileError {
0116   type: 'error';
0117   requestId: string;
0118   error: string;
0119 }
0120 
0121 export type RootFileResponse =
0122   | RootFileProbed
0123   | RootFileOpened
0124   | RootFileConverted
0125   | RootFileClosed
0126   | RootFileError;
0127 
0128 /** An open file as the session needs it: what Root2DexConverter offers. */
0129 export interface OpenedConverter {
0130   readonly sourceName: string;
0131   readonly model: PodioModel;
0132   readonly entryCount: number;
0133   convert(
0134     entries: number[],
0135     options: RootConvertOptions & { onWarning?: (message: string) => void },
0136   ): Promise<DexDocument>;
0137 }
0138 
0139 /** What the session reads files with; the worker passes jsroot and root2dex. */
0140 export interface RootFileBackend {
0141   /** Lists a file's top-level keys with their class names. */
0142   listKeys(source: File | string): Promise<ContentEntry[]>;
0143   /** Opens a podio file for conversion. */
0144   open(source: File | string): Promise<OpenedConverter>;
0145   /** The collection groups a model's conversion knows. */
0146   collectionGroups(model: PodioModel): string[];
0147 }
0148 
0149 /** Answers RootFileRequests: one session per worker. */
0150 export class RootFileSession {
0151   /** Open files by handle. */
0152   private readonly converters = new Map<number, OpenedConverter>();
0153   /** The latest open request per handle: an older open that finishes later is not kept. */
0154   private readonly latestOpen = new Map<number, string>();
0155 
0156   constructor(
0157     private readonly backend: RootFileBackend,
0158     private readonly post: (message: RootFileResponse) => void,
0159   ) {}
0160 
0161   /** Handles one request; the answer, or an error, goes out through `post`. */
0162   async handle(request: RootFileRequest): Promise<void> {
0163     try {
0164       switch (request.type) {
0165         case 'probe':
0166           return await this.probe(request);
0167         case 'open':
0168           return await this.open(request);
0169         case 'convert':
0170           return await this.convert(request);
0171         case 'close':
0172           this.converters.delete(request.handle);
0173           this.latestOpen.delete(request.handle);
0174           this.post({ type: 'closed', requestId: request.requestId });
0175           return;
0176       }
0177     } catch (error) {
0178       this.post({
0179         type: 'error',
0180         requestId: request.requestId,
0181         error: error instanceof Error ? error.message : String(error),
0182       });
0183     }
0184   }
0185 
0186   /**
0187    * Lists the file's top-level keys. No interpretation: a detector geometry
0188    * comes back as `{name: 'Default', className: 'TGeoManager'}` and event data
0189    * as `{name: 'events', className: 'TTree'}`, and the caller decides what
0190    * that means.
0191    */
0192   private async probe(request: RootFileProbeRequest): Promise<void> {
0193     const entries = await this.backend.listKeys(sourceOf(request.source));
0194     const probe: FileContentProbe = { name: nameOf(request.source), entries };
0195     this.post({ type: 'probed', requestId: request.requestId, probe });
0196   }
0197 
0198   private async open(request: RootFileOpenRequest): Promise<void> {
0199     this.latestOpen.set(request.handle, request.requestId);
0200     const converter = await this.backend.open(sourceOf(request.source));
0201     if (this.latestOpen.get(request.handle) === request.requestId) {
0202       this.converters.set(request.handle, converter);
0203     }
0204     this.post({
0205       type: 'opened',
0206       requestId: request.requestId,
0207       sourceName: converter.sourceName,
0208       model: converter.model,
0209       entryCount: converter.entryCount,
0210       collectionGroups: this.backend.collectionGroups(converter.model),
0211     });
0212   }
0213 
0214   private async convert(request: RootFileConvertRequest): Promise<void> {
0215     const converter = this.converters.get(request.handle);
0216     if (!converter) throw new Error('No ROOT file is open');
0217     // The size is checked and the ranges clamped before the selection is expanded
0218     const { entries, warning } = selectEntries(parseEntryRanges(request.entries), converter.entryCount, DEFAULT_MAX_ENTRIES);
0219     const warnings: string[] = warning ? [warning] : [];
0220     const convertStart = performance.now();
0221     const dex = await converter.convert(entries, {
0222       collections: request.collections?.length ? request.collections : undefined,
0223       trajectoryExcludedCollections: request.trajectoryExcludedCollections,
0224       onWarning: message => warnings.push(message),
0225     });
0226     const convertMs = performance.now() - convertStart;
0227     this.post({
0228       type: 'converted',
0229       requestId: request.requestId,
0230       entries,
0231       dex,
0232       warnings,
0233       convertMs,
0234       postedAtMs: Date.now(),
0235     });
0236   }
0237 }
0238 
0239 function sourceOf(source: RootFileSource): File | string {
0240   return source.kind === 'file' ? source.file : source.url;
0241 }
0242 
0243 function nameOf(source: RootFileSource): string {
0244   return source.kind === 'file' ? source.file.name : source.url;
0245 }