LCOV - code coverage report
Current view: top level - dashql-app/src/session/scripts - session_scripts_registry.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 91.7 % 266 244
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import { forgetSessionScriptReferences } from './script_reference_reports.js';
       4              : import { forgetDataflowGraph } from '../dataflow/dataflow_graph.js';
       5              : import { ANALYZE_OUTDATED_SCRIPT, SessionScripts, SessionScriptsAction, ScriptData, destroySessionScripts, isAnalysisOutdated, newScriptConnection, promoteEphemeralSession, reduceSessionScripts, SessionScriptsEffects, sessionConnectionIds, sessionScriptListWrite } from './session_scripts.js';
       6              : import { Dispatch } from '../../utils/variant.js';
       7              : import { UpdateQueue, useStore, useStoreState, useUpdateQueue } from '../../utils/store.js';
       8              : import { useStorageWriter } from '../../storage/storage_provider.js';
       9              : import { ConnectionReferencesProvider, type ConnectionReferences } from '../../connections/connection_registry.js';
      10              : import { useLogger } from '../../platform/logger/logger_provider.js';
      11              : import { stringifyError } from '../../platform/logger/logger.js';
      12              : import type { InvalidSession } from '../../storage/session_validation.js';
      13              : import {
      14              :     REPLACE_SESSION_SCRIPTS,
      15              :     DEBOUNCE_DURATION_SESSION_WRITE,
      16              :     type SessionReplacement,
      17              :     groupSessionScriptListWrites,
      18              :     groupSessionWrites,
      19              :     WRITE_SESSION_SCRIPTS,
      20              : } from "../../storage/storage_writer.js";
      21              : 
      22            1 : const LOG_CTX = 'session_scripts_registry';
      23              : 
      24              : /// The scripts registry.
      25              : ///
      26              : /// Note that we're deliberately not using immutable maps for session scripts and the connection index.
      27              : /// We're never "observing" these maps directly and thus can live with the simple variants.
      28              : /// Shallow-compare the entire registry object instead when reacting to session list changes.
      29              : export interface SessionScriptsRegistry {
      30              :     /// The scripts map (sessionId -> SessionScripts)
      31              :     sessionScriptsMap: Map<string, SessionScripts>;
      32              :     /// The index to find the sessions with scripts that analyze with a connection (connectionId -> sessionIds)
      33              :     sessionScriptsByConnection: Map<string, string[]>;
      34              :     /// The sessions that were refused a load (sessionId -> reason). They never enter the scripts map,
      35              :     /// the workbench lists them as invalid and they can only be deleted.
      36              :     invalidSessions: Map<string, InvalidSession>;
      37              : }
      38              : 
      39              : export type SetSessionScriptsRegistryAction = React.SetStateAction<SessionScriptsRegistry>;
      40              : 
      41              : /// Copy the indices of a registry before modifying them. Snapshots of the registry are never modified, a
      42              : /// component that renders an older snapshot keeps seeing the maps of that snapshot.
      43            1 : export function copySessionScriptsRegistry(reg: SessionScriptsRegistry): SessionScriptsRegistry {
      44            1 :     return {
      45            1 :         ...reg,
      46            1 :         sessionScriptsMap: new Map(reg.sessionScriptsMap),
      47            1 :         sessionScriptsByConnection: new Map(reg.sessionScriptsByConnection),
      48            1 :     };
      49            1 : }
      50              : 
      51              : /// The registry with the next scripts of a session, the given registry stays unchanged
      52            1 : export function replaceSessionScriptsInRegistry(reg: SessionScriptsRegistry, prev: SessionScripts, next: SessionScripts): SessionScriptsRegistry {
      53            1 :     const copy = copySessionScriptsRegistry(reg);
      54            1 :     copy.sessionScriptsMap.set(next.sessionId, next);
      55            1 :     reindexSessionScriptConnections(copy, prev, next);
      56            1 :     return copy;
      57            1 : }
      58              : export type SessionScriptsAllocator = (scripts: SessionScripts) => [string, SessionScripts];
      59              : export type ModifySessionScripts = (action: SessionScriptsAction) => Promise<SessionScripts | null> | void;
      60              : /// Resolves once the action was applied to the sessions
      61              : export type ModifyConnectionSessionScripts = (conn: string, action: SessionScriptsAction) => Promise<void>;
      62              : 
      63              : // The registry changes with every editor update while the setter is stable.
      64              : // Hooks that only modify the registry subscribe to the setter context and don't re-render on every update.
      65            1 : const SESSION_SCRIPTS_REGISTRY_CTX = React.createContext<SessionScriptsRegistry | null>(null);
      66            1 : const SESSION_SCRIPTS_REGISTRY_SETTER_CTX = React.createContext<Dispatch<SetSessionScriptsRegistryAction> | null>(null);
      67              : // Reads the current registry without subscribing to its changes
      68            1 : const SESSION_SCRIPTS_REGISTRY_READER_CTX = React.createContext<(() => SessionScriptsRegistry) | null>(null);
      69              : // The session script actions of all components are applied in order through one queue
      70            1 : const SESSION_SCRIPTS_REGISTRY_QUEUE_CTX = React.createContext<UpdateQueue<SessionScriptsRegistry> | null>(null);
      71              : 
      72              : type Props = {};
      73              : 
      74            1 : export const SessionScriptsRegistryProvider: React.FC<React.PropsWithChildren<Props>> = (props: React.PropsWithChildren<Props>) => {
      75              :     // Reducing session scripts mutates Wasm objects and schedules storage writes, the store runs every update once
      76            1 :     const store = useStore<SessionScriptsRegistry>(() => ({
      77            1 :         sessionScriptsMap: new Map(),
      78            1 :         sessionScriptsByConnection: new Map(),
      79            1 :         invalidSessions: new Map(),
      80            1 :     }));
      81            1 :     const queue = useUpdateQueue(store);
      82            1 :     const reg = useStoreState(store);
      83              :     // The scripts own the references to their connections, the connection layer reads them from here
      84            1 :     const references = React.useMemo<ConnectionReferences>(() => sessionConnectionReferences(store), [store]);
      85            1 :     return (
      86            1 :         <ConnectionReferencesProvider value={references}>
      87            1 :             <SESSION_SCRIPTS_REGISTRY_QUEUE_CTX.Provider value={queue}>
      88            1 :                 <SESSION_SCRIPTS_REGISTRY_READER_CTX.Provider value={store.get}>
      89            1 :                     <SESSION_SCRIPTS_REGISTRY_SETTER_CTX.Provider value={store.update}>
      90            1 :                         <SESSION_SCRIPTS_REGISTRY_CTX.Provider value={reg}>
      91            1 :                             {props.children}
      92            1 :                         </SESSION_SCRIPTS_REGISTRY_CTX.Provider>
      93            1 :                     </SESSION_SCRIPTS_REGISTRY_SETTER_CTX.Provider>
      94            1 :                 </SESSION_SCRIPTS_REGISTRY_READER_CTX.Provider>
      95            1 :             </SESSION_SCRIPTS_REGISTRY_QUEUE_CTX.Provider>
      96            1 :         </ConnectionReferencesProvider>
      97              :     );
      98            1 : };
      99              : 
     100              : /// The connections that the sessions of a scripts registry use
     101            1 : export function sessionConnectionReferences(registry: { current: SessionScriptsRegistry; subscribe: (listener: () => void) => () => void }): ConnectionReferences {
     102            1 :     return {
     103            1 :         sessionsOf: connectionId => registry.current.sessionScriptsByConnection.get(connectionId) ?? [],
     104            1 :         connectionsOf: sessionId => {
     105            1 :             const scripts = registry.current.sessionScriptsMap.get(sessionId);
     106            1 :             return scripts == null ? [] : sessionConnectionIds(scripts);
     107            1 :         },
     108            1 :         subscribe: registry.subscribe,
     109            1 :     };
     110            1 : }
     111              : 
     112            1 : export function useSessionScriptsRegistry(): [SessionScriptsRegistry, Dispatch<SetSessionScriptsRegistryAction>] {
     113            1 :     return [React.useContext(SESSION_SCRIPTS_REGISTRY_CTX)!, React.useContext(SESSION_SCRIPTS_REGISTRY_SETTER_CTX)!];
     114            1 : }
     115              : 
     116              : /// Modify the registry without subscribing to its changes
     117            1 : export function useSessionScriptsRegistrySetter(): Dispatch<SetSessionScriptsRegistryAction> {
     118            1 :     return React.useContext(SESSION_SCRIPTS_REGISTRY_SETTER_CTX)!;
     119            1 : }
     120              : 
     121              : /// Read the current registry without subscribing to its changes
     122            1 : export function useSessionScriptsRegistryReader(): () => SessionScriptsRegistry {
     123            1 :     return React.useContext(SESSION_SCRIPTS_REGISTRY_READER_CTX)!;
     124            1 : }
     125              : 
     126              : /// The sessions that were refused a load
     127              : export interface InvalidSessions {
     128              :     /// The invalid sessions (sessionId -> reason)
     129              :     invalidSessions: ReadonlyMap<string, InvalidSession>;
     130              :     /// Delete the files of an invalid session and drop it from the list
     131              :     deleteInvalidSession: (sessionId: string) => Promise<void>;
     132              : }
     133              : 
     134            1 : export function useInvalidSessions(): InvalidSessions {
     135            1 :     const reg = React.useContext(SESSION_SCRIPTS_REGISTRY_CTX)!;
     136            1 :     const setReg = React.useContext(SESSION_SCRIPTS_REGISTRY_SETTER_CTX)!;
     137            1 :     const storage = useStorageWriter();
     138            1 :     const logger = useLogger();
     139            1 :     const deleteInvalidSession = React.useCallback(async (sessionId: string) => {
     140            0 :         try {
     141            0 :             await storage.deleteSession(sessionId);
     142            0 :         } catch (error) {
     143            0 :             logger.error('Failed to delete invalid session', { sessionId, error: stringifyError(error) }, LOG_CTX);
     144            0 :         }
     145            0 :         setReg(reg => {
     146            0 :             if (!reg.invalidSessions.has(sessionId)) return reg;
     147            0 :             const invalidSessions = new Map(reg.invalidSessions);
     148            0 :             invalidSessions.delete(sessionId);
     149            0 :             return { ...reg, invalidSessions };
     150            0 :         });
     151            1 :     }, [logger, setReg, storage]);
     152            1 :     return React.useMemo(() => ({ invalidSessions: reg.invalidSessions, deleteInvalidSession }), [reg.invalidSessions, deleteInvalidSession]);
     153            1 : }
     154              : 
     155            1 : export function useSessionScriptsAllocator(): SessionScriptsAllocator {
     156            1 :     const storage = useStorageWriter();
     157            1 :     const setReg = React.useContext(SESSION_SCRIPTS_REGISTRY_SETTER_CTX)!;
     158            1 :     return React.useCallback((state: SessionScripts) => {
     159            1 :         const sessionId = state.sessionId;
     160            1 :         const scripts: SessionScripts = { ...state };
     161              : 
     162              :         // Modify the registry
     163            1 :         setReg((prevReg) => {
     164            1 :             const connectorInfo = newScriptConnection(scripts, null)?.connectorInfo;
     165            1 :             if (scripts.sessionMetadata.originalFileName == "" && connectorInfo != null) {
     166            1 :                 scripts.sessionMetadata.originalFileName = `${connectorInfo.names.fileShort}`;
     167              :             }
     168              : 
     169            1 :             const reg = copySessionScriptsRegistry(prevReg);
     170            1 :             for (const connectionId of sessionConnectionIds(scripts)) {
     171            1 :                 addSessionScriptsForConnection(reg, connectionId, sessionId);
     172              :             }
     173            1 :             reg.sessionScriptsMap.set(sessionId, scripts);
     174            1 :             return reg;
     175            1 :         });
     176              : 
     177            1 :         storage.write(groupSessionWrites(scripts.sessionId), {
     178            1 :             type: REPLACE_SESSION_SCRIPTS,
     179            1 :             value: sessionReplacement(scripts),
     180            1 :         }, DEBOUNCE_DURATION_SESSION_WRITE);
     181              :         // Scheduled after the session file is written, the script list has a single writer
     182            1 :         storage.write(groupSessionScriptListWrites(scripts.sessionId), {
     183            1 :             type: WRITE_SESSION_SCRIPTS,
     184            1 :             value: [scripts.sessionId, sessionScriptListWrite(scripts)],
     185            1 :         }, DEBOUNCE_DURATION_SESSION_WRITE);
     186            1 :         return [sessionId, scripts];
     187            1 :     }, [setReg, storage]);
     188            1 : }
     189              : 
     190              : /// The write of a whole session, its script texts are read when the write runs
     191            1 : export function sessionReplacement(scripts: SessionScripts): SessionReplacement {
     192            1 :     return {
     193            1 :         sessionId: scripts.sessionId,
     194            1 :         name: scripts.name,
     195            1 :         scripts: scripts.scriptOrder.flatMap(fileName => {
     196            0 :             const script = scripts.scripts[scripts.scriptRefs[fileName]?.scriptId];
     197            0 :             return script == null ? [] : [{
     198            0 :                 fileName,
     199            0 :                 readText: () => script.scriptSession.getText(),
     200            0 :             }];
     201            1 :         }),
     202            1 :         connection: sessionScriptListWrite(scripts).connections[0] ?? null,
     203            1 :         originalFileName: scripts.sessionMetadata.originalFileName,
     204            1 :     };
     205            1 : }
     206              : 
     207              : /// Record that a session analyzes its scripts with a connection (mutates the registry)
     208            1 : export function addSessionScriptsForConnection(reg: SessionScriptsRegistry, connectionId: string, sessionId: string): void {
     209            1 :     const sessions = reg.sessionScriptsByConnection.get(connectionId) ?? [];
     210            1 :     if (!sessions.includes(sessionId)) {
     211            1 :         reg.sessionScriptsByConnection.set(connectionId, [...sessions, sessionId]);
     212              :     }
     213            1 : }
     214              : 
     215              : /// Follow the connections of a session's scripts in the connection index (mutates the registry)
     216            1 : export function reindexSessionScriptConnections(reg: SessionScriptsRegistry, prev: SessionScripts, next: SessionScripts): void {
     217            1 :     if (prev.scripts === next.scripts && prev.pendingConnection === next.pendingConnection) return;
     218            1 :     const prevIds = new Set(sessionConnectionIds(prev));
     219            1 :     const nextIds = new Set(sessionConnectionIds(next));
     220            1 :     for (const connectionId of prevIds) {
     221            1 :         if (!nextIds.has(connectionId)) removeSessionScriptsForConnection(reg, connectionId, next.sessionId);
     222              :     }
     223            1 :     for (const connectionId of nextIds) {
     224            1 :         if (!prevIds.has(connectionId)) addSessionScriptsForConnection(reg, connectionId, next.sessionId);
     225              :     }
     226            1 : }
     227              : 
     228              : /// Drop a session from the scripts of a connection (mutates the registry)
     229            1 : export function removeSessionScriptsForConnection(reg: SessionScriptsRegistry, connectionId: string, sessionId: string): void {
     230            1 :     const sessions = (reg.sessionScriptsByConnection.get(connectionId) ?? []).filter(id => id !== sessionId);
     231            1 :     if (sessions.length === 0) {
     232            1 :         reg.sessionScriptsByConnection.delete(connectionId);
     233              :     } else {
     234            1 :         reg.sessionScriptsByConnection.set(connectionId, sessions);
     235              :     }
     236            1 : }
     237              : 
     238              : /// Remove a session from all three registry indices.
     239              : ///
     240              : /// Pure and idempotent (a missing entry is a no-op, the given registry stays unchanged), so it is safe to run
     241              : /// inside a React state updater that may be invoked more than once. It touches no Wasm — freeing the session's Wasm is
     242              : /// the caller's responsibility and must happen separately (see useSessionScriptsDeletion), because that
     243              : /// teardown is order-sensitive against the shared connection catalog.
     244            1 : export function removeSessionScriptsFromRegistry(reg: SessionScriptsRegistry, sessionId: string): SessionScriptsRegistry {
     245            1 :     const entry = reg.sessionScriptsMap.get(sessionId);
     246            1 :     if (!entry) return reg;
     247            1 :     const next = copySessionScriptsRegistry(reg);
     248            1 :     next.sessionScriptsMap.delete(sessionId);
     249            1 :     for (const connectionId of sessionConnectionIds(entry)) {
     250            1 :         removeSessionScriptsForConnection(next, connectionId, sessionId);
     251              :     }
     252            1 :     return next;
     253            1 : }
     254              : 
     255              : /// Delete a session's scripts and free their Wasm.
     256              : ///
     257              : /// Session scripts share the connection's catalog by reference (see session_creation) and own every
     258              : /// editor session they create. destroySessionScripts() drops those editor sessions from the shared catalog and then frees
     259              : /// them, so it MUST run while that catalog is still alive, i.e.
     260              : /// *before* the connection is deleted (DELETE_CONNECTION destroys the catalog). We therefore tear
     261              : /// the Wasm down synchronously here, in the event handler, and keep the registry-map removal a
     262              : /// pure updater (safe to run more than once). Callers must invoke this before dispatching
     263              : /// DELETE_CONNECTION for the same session.
     264            1 : export function useSessionScriptsDeletion(): (sessionId: string) => void {
     265            1 :     const readReg = React.useContext(SESSION_SCRIPTS_REGISTRY_READER_CTX)!;
     266            1 :     const setReg = React.useContext(SESSION_SCRIPTS_REGISTRY_SETTER_CTX)!;
     267            1 :     return React.useCallback((sessionId: string) => {
     268              :         // Free the session-owned Wasm now, before the shared catalog can be destroyed. The store holds the
     269              :         // latest registry, a rendered snapshot may miss the latest scripts of the session.
     270            0 :         const scripts = readReg().sessionScriptsMap.get(sessionId);
     271            0 :         if (scripts) {
     272            0 :             destroySessionScripts(scripts);
     273              :         }
     274              :         // The caches of the session in the app go with it
     275            0 :         forgetSessionScriptReferences(sessionId);
     276            0 :         forgetDataflowGraph(sessionId);
     277              :         // Drop the entry from all three indices.
     278            0 :         setReg((prev) => removeSessionScriptsFromRegistry(prev, sessionId));
     279            1 :     }, [readReg, setReg]);
     280            1 : }
     281              : 
     282              : /// Apply an action to the scripts of a session through the shared queue
     283            1 : function dispatchSessionScriptsAction(
     284            1 :     queue: UpdateQueue<SessionScriptsRegistry>,
     285            1 :     storageWriter: ReturnType<typeof useStorageWriter>,
     286            1 :     logger: ReturnType<typeof useLogger>,
     287            1 :     id: string,
     288            1 :     action: SessionScriptsAction,
     289            1 : ): Promise<SessionScripts | null> {
     290            1 :     return new Promise<SessionScripts | null>((resolve) => {
     291            1 :         logger.debug('Queued session script action', {
     292            1 :             sessionId: id,
     293            1 :             actionType: action.type.description ?? action.type.toString(),
     294            1 :         }, LOG_CTX);
     295            1 :         queue.push(reg => {
     296            1 :             const prev = reg.sessionScriptsMap.get(id);
     297            1 :             if (!prev) {
     298            1 :                 logger.warn('No session scripts registered for session', { sessionId: id }, LOG_CTX);
     299            1 :                 resolve(null);
     300            1 :                 return reg;
     301            1 :             }
     302            1 :             let next: SessionScripts;
     303            1 :             const effects = new SessionScriptsEffects(storageWriter);
     304            1 :             try {
     305            1 :                 next = promoteEphemeralSession(reduceSessionScripts(prev, action, effects, logger), reg.sessionScriptsMap.values(), effects);
     306            1 :             } catch (e: unknown) {
     307              :                 // Drop the action, a throw would also drop the other updates of the queue flush
     308            1 :                 logger.error('Session script action failed', {
     309            1 :                     sessionId: id,
     310            1 :                     actionType: action.type.description ?? action.type.toString(),
     311            1 :                     error: stringifyError(e),
     312            1 :                 }, LOG_CTX);
     313            1 :                 resolve(prev);
     314            1 :                 return reg;
     315            1 :             }
     316            1 :             effects.commit();
     317            1 :             resolve(next);
     318              :             // Keep the registry identity for no-ops (e.g. re-selecting the hovered script)
     319            1 :             if (next === prev) return reg;
     320            1 :             return replaceSessionScriptsInRegistry(reg, prev, next);
     321            1 :         });
     322            1 :     });
     323            1 : }
     324              : 
     325            1 : export function useSessionScripts(id: string | null): [SessionScripts | null, ModifySessionScripts] {
     326            1 :     const registry = React.useContext(SESSION_SCRIPTS_REGISTRY_CTX)!;
     327            1 :     const queue = React.useContext(SESSION_SCRIPTS_REGISTRY_QUEUE_CTX)!;
     328            1 :     const storageWriter = useStorageWriter();
     329            1 :     const logger = useLogger();
     330              : 
     331              :     /// Wrapper to modify an individual session scripts collection
     332            1 :     const dispatch = React.useCallback((action: SessionScriptsAction) => {
     333            1 :         if (id == null) return Promise.resolve(null);
     334            1 :         return dispatchSessionScriptsAction(queue, storageWriter, logger, id, action);
     335            1 :     }, [id, queue, storageWriter, logger]);
     336              : 
     337            1 :     return [id == null ? null : registry.sessionScriptsMap.get(id) ?? null, dispatch];
     338            1 : };
     339              : 
     340              : /// Modify the scripts of any session without subscribing to the registry
     341            1 : export function useSessionScriptsDispatch(): (id: string, action: SessionScriptsAction) => Promise<SessionScripts | null> {
     342            1 :     const queue = React.useContext(SESSION_SCRIPTS_REGISTRY_QUEUE_CTX)!;
     343            1 :     const storageWriter = useStorageWriter();
     344            1 :     const logger = useLogger();
     345            1 :     return React.useCallback(
     346            1 :         (id: string, action: SessionScriptsAction) => dispatchSessionScriptsAction(queue, storageWriter, logger, id, action),
     347            1 :         [queue, storageWriter, logger],
     348            1 :     );
     349            1 : }
     350              : 
     351              : /// Return a script with current analysis, synchronizing session state first when necessary.
     352            1 : export async function ensureSessionScriptAnalyzed(
     353            1 :     sessionScripts: SessionScripts,
     354            1 :     scriptKey: number,
     355            1 :     modifySessionScripts: ModifySessionScripts,
     356            1 : ): Promise<ScriptData | null> {
     357            1 :     const scriptData = sessionScripts.scripts[scriptKey];
     358            1 :     if (!scriptData) return null;
     359            1 :     if (!isAnalysisOutdated(scriptData)) return scriptData;
     360            1 :     const result = modifySessionScripts({ type: ANALYZE_OUTDATED_SCRIPT, value: scriptKey });
     361            1 :     if (!result) return null;
     362            1 :     const next = await result;
     363            1 :     return next ? next.scripts[scriptKey] ?? null : null;
     364            1 : }
     365              : 
     366            1 : export function useConnectionScriptsDispatch(): ModifyConnectionSessionScripts {
     367            1 :     const queue = React.useContext(SESSION_SCRIPTS_REGISTRY_QUEUE_CTX)!;
     368            1 :     const storage = useStorageWriter();
     369            1 :     const logger = useLogger();
     370              : 
     371            1 :     return React.useCallback<ModifyConnectionSessionScripts>((conn: string, action: SessionScriptsAction) => new Promise<void>(resolve => {
     372            1 :         queue.push(prevReg => {
     373            1 :             let reg = prevReg;
     374              :             // Every session with scripts that analyze with the connection receives the action
     375            1 :             for (const sessionId of [...(reg.sessionScriptsByConnection.get(conn) ?? [])]) {
     376            1 :                 const prev = reg.sessionScriptsMap.get(sessionId);
     377            1 :                 if (prev) {
     378            1 :                     let next: SessionScripts;
     379            1 :                     const effects = new SessionScriptsEffects(storage);
     380            1 :                     try {
     381            1 :                         next = reduceSessionScripts(prev, action, effects, logger);
     382            1 :                     } catch (e: unknown) {
     383              :                         // Skip the session, the other sessions of the connection still receive the action
     384            1 :                         logger.error('Connection script action failed', {
     385            1 :                             connectionId: conn,
     386            1 :                             sessionId,
     387            1 :                             actionType: action.type.description ?? action.type.toString(),
     388            1 :                             error: stringifyError(e),
     389            1 :                         }, LOG_CTX);
     390            1 :                         continue;
     391            1 :                     }
     392            1 :                     effects.commit();
     393            1 :                     if (next !== prev) reg = replaceSessionScriptsInRegistry(reg, prev, next);
     394            1 :                 }
     395            1 :             }
     396            1 :             resolve();
     397            1 :             return reg;
     398            1 :         });
     399            1 :     }), [queue, storage, logger]);
     400            1 : }
        

Generated by: LCOV version 2.0-1