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