LCOV - code coverage report
Current view: top level - dashql-app/src/connections/hyper - hyper_link.ts Coverage Total Hit
Test: DashQL 915828b Lines: 98.7 % 77 76
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import type * as dashql from '@ankoh/dashql-core';
       3              : 
       4              : import { createConnectionStateFromParams, type ConnectionParams } from '../connection_params.js';
       5              : import type { ConnectionAllocator } from '../connection_registry.js';
       6              : import type { ConnectionState } from '../connection_state.js';
       7              : import { isLoopbackHyperLink, type HyperLinkTarget } from '../../platform/events/hyper_link_target.js';
       8              : 
       9              : export { hyperLinkChannelParams, isLoopbackHyperLink, parseHyperLinkTarget, type HyperLinkTarget } from '../../platform/events/hyper_link_target.js';
      10              : 
      11              : /// The session storage key of the tokens of the Hyper links of a tab
      12            1 : const HYPER_LINK_SESSION_STORAGE_KEY = 'dashql.hyper_links';
      13              : 
      14              : /// Confirm a link before the app uses it.
      15              : /// A link to another machine would receive the queries of the new sessions, the user confirms its host first.
      16            1 : export function confirmHyperLink(target: HyperLinkTarget, confirm: (message: string) => boolean = message => globalThis.confirm(message)): boolean {
      17            1 :     if (isLoopbackHyperLink(target)) return true;
      18            1 :     return confirm(`Run new sessions on the Hyper at ${target.endpoint}? Only continue if you trust the link that opened DashQL.`);
      19            1 : }
      20              : 
      21              : /// The Hyper links of a tab
      22              : export interface HyperLinkTabState {
      23              :     /// The tokens of the connections to the Hyper of a link (connectionId -> token), they are never stored durably
      24              :     tokens: Record<string, string>;
      25              :     /// The connection of the latest link, new sessions start on it
      26              :     current: string | null;
      27              : }
      28              : 
      29            1 : const NO_HYPER_LINKS: HyperLinkTabState = { tokens: {}, current: null };
      30              : 
      31              : /// The storage of the tab, null if the browser refuses it
      32            1 : function tabStorage(): Storage | null {
      33            1 :     try {
      34            1 :         return globalThis.sessionStorage ?? null;
      35            1 :     } catch {
      36            1 :         return null;
      37            1 :     }
      38            1 : }
      39              : 
      40              : /// Read the Hyper links of the tab, a reload keeps them and closing the tab ends them
      41            1 : export function readHyperLinkTabState(storage: Storage | null = tabStorage()): HyperLinkTabState {
      42            1 :     try {
      43            1 :         const parsed = JSON.parse(storage?.getItem(HYPER_LINK_SESSION_STORAGE_KEY) ?? 'null');
      44            1 :         if (parsed == null || typeof parsed !== 'object') return NO_HYPER_LINKS;
      45            1 :         const tokens = Object.fromEntries(Object.entries(parsed.tokens ?? {}).filter(([, token]) => typeof token === 'string')) as Record<string, string>;
      46            1 :         return { tokens, current: typeof parsed.current === 'string' ? parsed.current : null };
      47            1 :     } catch {
      48            1 :         return NO_HYPER_LINKS;
      49            1 :     }
      50            1 : }
      51              : 
      52            1 : let HYPER_LINK_TAB_STATE: HyperLinkTabState | null = null;
      53            1 : const HYPER_LINK_LISTENERS = new Set<() => void>();
      54              : 
      55              : /// Get the Hyper links of the tab
      56            1 : export function getHyperLinkTabState(): HyperLinkTabState {
      57            1 :     HYPER_LINK_TAB_STATE ??= readHyperLinkTabState();
      58            1 :     return HYPER_LINK_TAB_STATE;
      59            1 : }
      60              : 
      61              : /// Remember the token of the connection that a link was bound to, new sessions start on the connection
      62            1 : export function setHyperLinkToken(connectionId: string, token: string, storage: Storage | null = tabStorage()): void {
      63            1 :     const prev = getHyperLinkTabState();
      64            1 :     HYPER_LINK_TAB_STATE = { tokens: { ...prev.tokens, [connectionId]: token }, current: connectionId };
      65            1 :     try {
      66            1 :         storage?.setItem(HYPER_LINK_SESSION_STORAGE_KEY, JSON.stringify(HYPER_LINK_TAB_STATE));
      67            1 :     } catch {
      68              :         // Without storage, a reload forgets the links
      69            0 :     }
      70            1 :     for (const listener of HYPER_LINK_LISTENERS) listener();
      71            1 : }
      72              : 
      73              : /// Subscribe to the Hyper links of the tab
      74            1 : function subscribeHyperLinkTabState(listener: () => void): () => void {
      75            1 :     HYPER_LINK_LISTENERS.add(listener);
      76            1 :     return () => HYPER_LINK_LISTENERS.delete(listener);
      77            1 : }
      78              : 
      79              : /// The Hyper links of the tab, they change with every link
      80            1 : export function useHyperLinkTabState(): HyperLinkTabState {
      81            1 :     return React.useSyncExternalStore(subscribeHyperLinkTabState, getHyperLinkTabState);
      82            1 : }
      83              : 
      84              : /// Does a connection speak to the Hyper of a link?
      85            1 : export function isHyperLinkParams(params: ConnectionParams): boolean {
      86            1 :     return 'hyper' in params && params.hyper.protocol === 'V3_HTTP_LINK';
      87            1 : }
      88              : 
      89              : /// The params of a new connection to the Hyper of a link, its token is never stored
      90            1 : export function createHyperLinkConnectionParams(endpoint: string): ConnectionParams {
      91            1 :     return {
      92            1 :         hyper: {
      93            1 :             protocol: 'V3_HTTP_LINK',
      94            1 :             endpoint,
      95            1 :             tls: { clientKeyPath: '', clientCertPath: '', caCertsPath: '' },
      96            1 :         },
      97            1 :     };
      98            1 : }
      99              : 
     100              : /// The connection that a link is bound to
     101              : export interface HyperLinkBinding {
     102              :     /// The connection
     103              :     connection: ConnectionState;
     104              :     /// The params that the connection is saved with, null if they don't change
     105              :     params: ConnectionParams | null;
     106              : }
     107              : 
     108              : /// Bind a link to a connection.
     109              : ///
     110              : /// A connection to the endpoint of the link is reused. Otherwise the user may reuse the connection of an earlier link,
     111              : /// e.g. of an earlier run of hyperd on another port, which then moves to the endpoint of the link. Declining creates a
     112              : /// new connection, so the app can speak to several Hyper of links at once.
     113            1 : export function bindHyperLink(
     114            1 :     target: HyperLinkTarget,
     115            1 :     connections: Iterable<ConnectionState>,
     116            1 :     allocate: ConnectionAllocator,
     117            1 :     dql: dashql.DashQL,
     118            1 :     recentConnectionId: string | null,
     119            1 :     confirmReuse: (connection: ConnectionState) => boolean,
     120            1 : ): HyperLinkBinding {
     121            1 :     const linked = [...connections].filter(connection => isHyperLinkParams(connection.params));
     122            1 :     const endpointOf = (connection: ConnectionState) => 'hyper' in connection.params ? connection.params.hyper.endpoint : null;
     123            1 :     const same = linked.find(connection => endpointOf(connection) === target.endpoint);
     124            1 :     if (same != null) return { connection: same, params: null };
     125            1 :     const recent = linked.find(connection => connection.connectionId === recentConnectionId) ?? linked[linked.length - 1];
     126            1 :     if (recent != null && 'hyper' in recent.params && confirmReuse(recent)) {
     127            1 :         return { connection: recent, params: { hyper: { ...recent.params.hyper, endpoint: target.endpoint } } };
     128              :     }
     129            1 :     return {
     130            1 :         connection: allocate({ ...createConnectionStateFromParams(dql, createHyperLinkConnectionParams(target.endpoint)), active: true }),
     131            1 :         params: null,
     132            1 :     };
     133            1 : }
        

Generated by: LCOV version 2.0-1