LCOV - code coverage report
Current view: top level - dashql-app/src/connections - connection_client_functions.ts Coverage Total Hit
Test: DashQL d805a4b Lines: 100.0 % 59 59
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : import type { DashQLCatalog } from '@ankoh/dashql-core';
       2              : 
       3              : import type { HttpClient } from '../platform/http/http_client.js';
       4              : import type { Logger } from '../platform/logger/logger.js';
       5              : import type { QueryExecutionResponseStream } from '../query/query_execution_state.js';
       6              : import { ClientFunctionResultStream } from './clfn/clfn_result_stream.js';
       7              : import { explainClientFunctionCall, runClientFunctionCall } from './clfn/clfn_runtime.js';
       8              : import { ClientFunctionError, type ClientFunctionCallDescription } from './clfn/clfn_spec.js';
       9              : import type { ConnectionStateDetailsVariant } from './connection_state_details.js';
      10              : import type { ConnectionState } from './connection_state.js';
      11              : import { connectorOfDetails, type ClientFunctionBinding } from './connector_descriptor.js';
      12              : 
      13            1 : const LOG_CTX = 'connection_client_functions';
      14              : 
      15              : /// What a catalog holds of a binding
      16              : interface Registration {
      17              :     /// The binding
      18              :     binding: ClientFunctionBinding;
      19              :     /// The namespace, null while the spec loads or if it failed to load
      20              :     namespace: string | null;
      21              : }
      22              : /// The registrations of the catalogs. A catalog belongs to one connection, and clearing it keeps its specs.
      23            1 : const REGISTRATIONS = new WeakMap<DashQLCatalog, Registration>();
      24              : 
      25              : /// Make the catalog of a connection hold the client functions of its connector.
      26              : /// `onDidUpdate` is called when they changed, the scripts of the connection are then outdated.
      27            1 : export function ensureConnectionClientFunctions(
      28            1 :     connections: ReadonlyMap<string, ConnectionState>,
      29            1 :     connectionId: string,
      30            1 :     onDidUpdate: (connectionId: string) => void,
      31            1 :     logger: Logger,
      32            1 : ): void {
      33            1 :     const connection = connections.get(connectionId);
      34            1 :     if (connection == null) return;
      35            1 :     const { catalog } = connection;
      36            1 :     const binding = connectorOfDetails(connection.details).clientFunctions ?? null;
      37            1 :     const registered = REGISTRATIONS.get(catalog) ?? null;
      38            1 :     if ((registered?.binding ?? null) === binding) return;
      39              : 
      40              :     // The connection switched its connector
      41            1 :     if (registered != null) {
      42            1 :         REGISTRATIONS.delete(catalog);
      43            1 :         if (registered.namespace != null) {
      44            1 :             catalog.dropClientFunctions(registered.namespace);
      45            1 :             onDidUpdate(connectionId);
      46            1 :         }
      47            1 :     }
      48            1 :     if (binding == null) return;
      49              : 
      50            1 :     const registration: Registration = { binding, namespace: null };
      51            1 :     REGISTRATIONS.set(catalog, registration);
      52            1 :     void (async () => {
      53            1 :         try {
      54            1 :             const spec = await binding.loadSpec();
      55              :             // The connection may be gone by now, or have switched its connector
      56            1 :             if (connections.get(connectionId)?.catalog !== catalog || REGISTRATIONS.get(catalog) !== registration) return;
      57            1 :             registration.namespace = catalog.registerClientFunctions(JSON.stringify(spec));
      58            1 :             onDidUpdate(connectionId);
      59            1 :         } catch (error) {
      60              :             // It stays unregistered: a script then gets no checks and no completion, and a call goes to the backend. The next
      61              :             // change of the registry tries again, a chunk of the spec may only have failed to load.
      62            1 :             logger.error('Failed to register client functions', { connectionId, error: String(error) }, LOG_CTX);
      63            1 :             if (REGISTRATIONS.get(catalog) === registration) REGISTRATIONS.delete(catalog);
      64            1 :         }
      65            1 :     })();
      66            1 : }
      67              : 
      68              : /// What the client function calls of the app use
      69              : export interface ClientFunctionServices {
      70              :     http: HttpClient;
      71              :     logger: Logger;
      72              : }
      73              : 
      74              : /// Execute a client function call of a connection. The result is complete once the stream exists: a call with several
      75              : /// pages has its pages by then, and `EXPLAIN` has sent nothing.
      76            1 : export async function executeConnectionClientFunction(
      77            1 :     details: ConnectionStateDetailsVariant,
      78            1 :     description: ClientFunctionCallDescription,
      79            1 :     services: ClientFunctionServices,
      80            1 :     abort?: AbortSignal,
      81            1 : ): Promise<QueryExecutionResponseStream> {
      82            1 :     const connector = connectorOfDetails(details);
      83            1 :     const binding = connector.clientFunctions;
      84            1 :     if (binding == null) {
      85            1 :         throw new ClientFunctionError(`${connector.info.names.displayLong} connections have no client functions`);
      86              :     }
      87            1 :     const spec = await binding.loadSpec();
      88            1 :     if (spec.namespace !== description.namespace) {
      89            1 :         throw new ClientFunctionError(`${connector.info.names.displayLong} connections have no client functions in ${description.namespace}`);
      90              :     }
      91            1 :     const variables = () => binding.variables(details.value);
      92            1 :     const result = description.explain
      93            1 :         ? await explainClientFunctionCall(spec, description, { variables })
      94            1 :         : await runClientFunctionCall(spec, description, {
      95            1 :             http: services.http,
      96            1 :             resolveHost: host => binding.resolveHost(details.value, host),
      97            1 :             variables,
      98            1 :         }, abort);
      99            1 :     return new ClientFunctionResultStream(services.logger, result);
     100            1 : }
        

Generated by: LCOV version 2.0-1