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 : }
|