Line data Source code
1 : import * as React from 'react';
2 :
3 : import {
4 : ConnectionState,
5 : ConnectionAction,
6 : ConnectionStateWithoutId,
7 : CATALOG_UPDATE_PARTIALLY_SUCCEEDED,
8 : CATALOG_UPDATE_SUCCEEDED,
9 : DELETE_CONNECTION,
10 : SWITCH_CONNECTOR_TYPE,
11 : reduceConnectionState,
12 : SET_CONNECTION_DRAFT,
13 : DISCARD_CONNECTION_DRAFT,
14 : COMMIT_CONNECTION_PARAMS,
15 : } from './connection_state.js';
16 : import { QUERY_PROGRESS_UPDATED, QUERY_RECEIVED_BATCH } from '../query/query_execution_state.js';
17 : import { Dispatch } from '../utils/variant.js';
18 : import { UpdateQueue, useStore, useStoreState, useUpdateQueue } from '../utils/store.js';
19 : import { CONNECTOR_TYPES } from './connector_info.js';
20 : import { isHyperWasmConnection } from './connection_label.js';
21 : import { useStorageWriter } from '../storage/storage_provider.js';
22 : import { useLogger } from '../platform/logger/logger_provider.js';
23 : import type { Logger } from '../platform/logger/logger.js';
24 :
25 1 : const LOG_CTX = 'connection_registry';
26 : import {
27 : DEBOUNCE_DURATION_SESSION_WRITE,
28 : groupConnectionEntryWrites,
29 : groupConnectionFileWrites,
30 : groupSessionConnectionCopyWrites,
31 : WRITE_CONNECTION_ENTRY,
32 : WRITE_CONNECTION_FILE,
33 : PATCH_SESSION_CONNECTION,
34 : DELETE_CONNECTION_ENTRY,
35 : type StorageWriter,
36 : } from '../storage/storage_writer.js';
37 : import { STORAGE_CATALOG_CHANGES, STORAGE_CATALOG_FUNCTIONS, STORAGE_CATALOG_RELATIONS, type ConnectionEntry, type SessionConnection } from '../storage/storage_backend.js';
38 : import { connectionCatalogIsDurable, connectionParamsEqual, type ConnectionParams } from './connection_params.js';
39 :
40 : /// Drafts are written to the app manifest only, edits arrive with every keystroke
41 : export const DEBOUNCE_DURATION_CONNECTION_DRAFT_WRITE = 250;
42 :
43 : /// The connection registry
44 : ///
45 : /// Connections are entities of their own, keyed by the global connection
46 : /// id. The scripts of sessions run on them, several sessions
47 : /// may reference the same connection and then share its channel, catalog and queries.
48 : ///
49 : /// Note that we're deliberately not using immutable maps for the connections here.
50 : /// Following the same reasoning as with the session registry, we don't have code that
51 : /// explicitly observes modifications of the registry map.
52 : /// Instead, shallow-compare the entire registry object again.
53 : export interface ConnectionRegistry {
54 : connections: Map<string, ConnectionState>; // connectionId -> ConnectionState
55 : connectionsByType: string[][]; // arrays of databaseIds by connector type
56 : }
57 :
58 : /// The sessions that use the connections.
59 : ///
60 : /// The scripts of a session own the references to their connections, the session scripts registry
61 : /// provides them to the connection layer. A host without sessions (e.g. the shell) has no references.
62 : export interface ConnectionReferences {
63 : /// The sessions with scripts on a connection, in registration order
64 : sessionsOf(connectionId: string): readonly string[];
65 : /// The connections that the scripts of a session use in feed order, then the pending connection
66 : connectionsOf(sessionId: string): readonly string[];
67 : /// Subscribe to changes of the references
68 : subscribe(listener: () => void): () => void;
69 : }
70 :
71 : /// The references of a host without sessions
72 1 : export const NO_CONNECTION_REFERENCES: ConnectionReferences = {
73 1 : sessionsOf: () => [],
74 1 : connectionsOf: () => [],
75 1 : subscribe: () => () => { },
76 1 : };
77 :
78 1 : const CONNECTION_REFERENCES_CTX = React.createContext<ConnectionReferences>(NO_CONNECTION_REFERENCES);
79 : /// Provide the sessions that use the connections
80 1 : export const ConnectionReferencesProvider = CONNECTION_REFERENCES_CTX.Provider;
81 :
82 : /// Get the references without subscribing to their changes
83 1 : export function useConnectionReferences(): ConnectionReferences {
84 1 : return React.useContext(CONNECTION_REFERENCES_CTX);
85 1 : }
86 :
87 : /// Subscribe to a slice of the references.
88 : /// The selector must return values that keep their identity while unchanged.
89 1 : export function useConnectionReferencesSelector<T>(select: (references: ConnectionReferences) => T): T {
90 1 : const references = useConnectionReferences();
91 1 : return React.useSyncExternalStore(references.subscribe, () => select(references));
92 1 : }
93 :
94 : /// The connections of a session, null while a connection is not registered
95 1 : export function resolveSessionConnections(
96 1 : registry: Pick<ConnectionRegistry, 'connections'>,
97 1 : references: ConnectionReferences,
98 1 : sessionId: string | null,
99 1 : ): ConnectionState[] | null {
100 1 : if (sessionId == null) return null;
101 1 : const connections = references.connectionsOf(sessionId)
102 1 : .map(connectionId => registry.connections.get(connectionId));
103 1 : if (connections.length === 0 || connections.some(database => database == null)) return null;
104 1 : return connections as ConnectionState[];
105 1 : }
106 :
107 : /// Is a connection listed next to the sessions?
108 : /// Connections that sessions reference are listed, as are active connections that no session references.
109 1 : export function isListedConnection(
110 1 : registry: Pick<ConnectionRegistry, 'connections'>,
111 1 : references: ConnectionReferences,
112 1 : connectionId: string,
113 1 : ): boolean {
114 1 : const database = registry.connections.get(connectionId);
115 1 : if (database == null) return false;
116 1 : return references.sessionsOf(connectionId).length > 0 || database.active;
117 1 : }
118 :
119 : /// Register a connection in the registry indices (mutates the registry)
120 1 : export function registerConnection(registry: ConnectionRegistry, database: ConnectionState): void {
121 1 : registry.connections.set(database.connectionId, database);
122 1 : const byType = registry.connectionsByType[database.connectorInfo.connectorType];
123 1 : if (!byType.includes(database.connectionId)) byType.push(database.connectionId);
124 1 : }
125 :
126 : /// The queries a session issued on a connection, active ones first
127 1 : export function findSessionQueries(database: ConnectionState, sessionId: string, includeFinished: boolean = true): number[] {
128 1 : const active = database.queriesActiveOrdered.filter(queryId => database.queriesActive.get(queryId)?.queryMetadata.sessionId === sessionId);
129 1 : if (!includeFinished) return active;
130 0 : const finished = database.queriesFinishedOrdered.filter(queryId => database.queriesFinished.get(queryId)?.queryMetadata.sessionId === sessionId);
131 0 : return [...active, ...finished];
132 1 : }
133 :
134 : /// Build the manifest entry of a connection
135 1 : export function connectionEntryFromState(database: ConnectionState): ConnectionEntry {
136 1 : return {
137 1 : id: database.connectionId,
138 1 : params: database.params,
139 1 : ...(database.paramsDraft == null ? {} : { paramsDraft: database.paramsDraft }),
140 1 : ...(database.name == null ? {} : { name: database.name }),
141 1 : ...(database.cacheQueryResults == null ? {} : { cacheQueryResults: database.cacheQueryResults }),
142 1 : };
143 1 : }
144 :
145 : /// Build the copy of a connection that a session file stores
146 1 : export function sessionConnectionFromState(database: ConnectionState): SessionConnection {
147 1 : return {
148 1 : connectionId: database.connectionId,
149 1 : params: database.params,
150 1 : };
151 1 : }
152 :
153 : /// Did the saved params change?
154 1 : export function didConnectionParamsChange(prev: ConnectionState, next: ConnectionState): boolean {
155 1 : return !connectionParamsEqual(prev.params, next.params);
156 1 : }
157 :
158 :
159 : /// Did the persisted connection entry change?
160 1 : export function didPersistedConnectionChange(prev: ConnectionState, next: ConnectionState): boolean {
161 1 : if (!prev.active && next.active) return true;
162 1 : return didConnectionParamsChange(prev, next) || !connectionParamsEqual(prev.paramsDraft, next.paramsDraft) || prev.name !== next.name || prev.cacheQueryResults !== next.cacheQueryResults;
163 1 : }
164 :
165 : /// Write the saved params of a connection into the app manifest and into every session that
166 : /// references it. The writes are debounced and not awaited, Save awaits them explicitly.
167 1 : export function scheduleConnectionParamsWrites(references: ConnectionReferences, database: ConnectionState, storageWriter: StorageWriter): void {
168 1 : void storageWriter.write(
169 1 : groupConnectionEntryWrites(database.connectionId),
170 1 : { type: WRITE_CONNECTION_ENTRY, value: connectionEntryFromState(database) },
171 : DEBOUNCE_DURATION_SESSION_WRITE,
172 1 : );
173 1 : for (const sessionId of references.sessionsOf(database.connectionId)) {
174 0 : void storageWriter.write(
175 0 : groupSessionConnectionCopyWrites(sessionId, database.connectionId),
176 0 : { type: PATCH_SESSION_CONNECTION, value: [sessionId, sessionConnectionFromState(database)] },
177 : DEBOUNCE_DURATION_SESSION_WRITE,
178 0 : );
179 : }
180 1 : }
181 :
182 : export type SetConnectionRegistryAction = React.SetStateAction<ConnectionRegistry>;
183 : export type ConnectionDispatch = (action: ConnectionAction) => void;
184 : export type DynamicConnectionDispatch = (connectionId: string | null, action: ConnectionAction) => void;
185 :
186 : /// A stable handle to the latest registry.
187 : ///
188 : /// The registry changes with every query lifecycle event (including the internal SQLFrame queries of
189 : /// computations), which re-renders every context consumer.
190 : /// Components that only care about a small slice of the registry subscribe to the store instead.
191 : export interface ConnectionRegistryStore {
192 : /// The latest registry
193 : current: ConnectionRegistry;
194 : /// Subscribe to registry changes
195 : subscribe: (listener: () => void) => () => void;
196 : }
197 :
198 1 : const CONNECTION_REGISTRY_CTX = React.createContext<ConnectionRegistry | null>(null);
199 1 : const CONNECTION_REGISTRY_SETTER_CTX = React.createContext<Dispatch<SetConnectionRegistryAction> | null>(null);
200 1 : const CONNECTION_REGISTRY_STORE_CTX = React.createContext<ConnectionRegistryStore | null>(null);
201 1 : const CONNECTION_REGISTRY_QUEUE_CTX = React.createContext<UpdateQueue<ConnectionRegistry> | null>(null);
202 :
203 : type Props = {
204 : children: React.ReactElement | React.ReactElement[];
205 : };
206 :
207 1 : export const ConnectionRegistry: React.FC<Props> = (props: Props) => {
208 : // Registry updates free Wasm objects and schedule storage writes, the store runs every update once
209 1 : const store = useStore<ConnectionRegistry>(() => ({
210 1 : connections: new Map(),
211 1 : connectionsByType: CONNECTOR_TYPES.map(() => ([])),
212 1 : }));
213 1 : const queue = useUpdateQueue(store);
214 1 : const reg = useStoreState(store);
215 1 : return (
216 1 : <CONNECTION_REGISTRY_STORE_CTX.Provider value={store}>
217 1 : <CONNECTION_REGISTRY_QUEUE_CTX.Provider value={queue}>
218 1 : <CONNECTION_REGISTRY_SETTER_CTX.Provider value={store.update}>
219 1 : <CONNECTION_REGISTRY_CTX.Provider value={reg}>
220 1 : {props.children}
221 1 : </CONNECTION_REGISTRY_CTX.Provider>
222 1 : </CONNECTION_REGISTRY_SETTER_CTX.Provider>
223 1 : </CONNECTION_REGISTRY_QUEUE_CTX.Provider>
224 1 : </CONNECTION_REGISTRY_STORE_CTX.Provider>
225 : );
226 1 : };
227 :
228 : /// Get the registry store without subscribing to registry changes
229 1 : export function useConnectionRegistryStore(): ConnectionRegistryStore {
230 1 : return React.useContext(CONNECTION_REGISTRY_STORE_CTX)!;
231 1 : }
232 :
233 : /// Subscribe to a slice of the registry.
234 : /// The component only re-renders when the selected value changes.
235 : /// The selector must return values that keep their identity while unchanged.
236 : export function useConnectionRegistrySelector<T>(select: (registry: ConnectionRegistry) => T): T {
237 : const store = useConnectionRegistryStore();
238 : return React.useSyncExternalStore(store.subscribe, () => select(store.current));
239 : }
240 :
241 : /// Modify the registry without subscribing to registry changes
242 1 : export function useConnectionRegistrySetter(): Dispatch<SetConnectionRegistryAction> {
243 1 : return React.useContext(CONNECTION_REGISTRY_SETTER_CTX)!;
244 1 : }
245 :
246 : export type ConnectionAllocator = (state: ConnectionStateWithoutId, connectionId?: string) => ConnectionState;
247 :
248 : /// Allocate a new connection that no session references yet.
249 : /// A session references the connection once its scripts use it.
250 : /// Active connections are written into the app manifest right away.
251 1 : export function useConnectionAllocator(): ConnectionAllocator {
252 1 : const setReg = React.useContext(CONNECTION_REGISTRY_SETTER_CTX)!;
253 1 : const storageWriter = useStorageWriter();
254 1 : return React.useCallback((state: ConnectionStateWithoutId, fixedId?: string) => {
255 1 : const connectionId = fixedId ?? crypto.randomUUID();
256 1 : const database: ConnectionState = { ...state, connectionId };
257 1 : setReg((reg) => {
258 1 : registerConnection(reg, database);
259 1 : return { ...reg };
260 1 : });
261 1 : if (database.active) {
262 1 : void storageWriter.write(
263 1 : groupConnectionEntryWrites(connectionId),
264 1 : { type: WRITE_CONNECTION_ENTRY, value: { ...connectionEntryFromState(database), createdAt: new Date().toISOString() } },
265 1 : );
266 : }
267 1 : return database;
268 1 : }, [setReg, storageWriter]);
269 1 : }
270 :
271 1 : export function useConnectionRegistry(): [ConnectionRegistry, Dispatch<SetConnectionRegistryAction>] {
272 1 : return [React.useContext(CONNECTION_REGISTRY_CTX)!, React.useContext(CONNECTION_REGISTRY_SETTER_CTX)!];
273 1 : }
274 :
275 1 : export function useDynamicConnectionDispatch(): [ConnectionRegistry, DynamicConnectionDispatch] {
276 1 : const registry = React.useContext(CONNECTION_REGISTRY_CTX)!;
277 1 : const dispatch = useConnectionDispatch();
278 1 : return [registry, dispatch];
279 1 : }
280 :
281 : /// Apply an action to a connection of the registry (mutates the registry)
282 1 : function applyConnectionAction(reg: ConnectionRegistry, references: ConnectionReferences, connectionId: string, action: ConnectionAction, storageWriter: StorageWriter, logger: Logger): ConnectionRegistry {
283 1 : const prev = reg.connections.get(connectionId);
284 1 : if (!prev) {
285 1 : console.warn(`no connection registered with id ${connectionId}`);
286 1 : return reg;
287 1 : }
288 1 : const connectorType = prev.connectorInfo.connectorType;
289 1 : const sessionIds = references.sessionsOf(connectionId);
290 :
291 : // Connections outlive the sessions that reference them, but a referenced connection
292 : // must neither be deleted nor change its type underneath other sessions.
293 1 : if (action.type == DELETE_CONNECTION && sessionIds.length > 0) {
294 1 : logger.warn('refusing to delete a connection that sessions reference', {
295 1 : connectionId,
296 1 : sessions: sessionIds.join(','),
297 1 : }, LOG_CTX);
298 1 : return reg;
299 1 : }
300 : // Sessions fall back to the embedded Hyper and the user can't attach one again, it is never deleted
301 1 : if (action.type == DELETE_CONNECTION && isHyperWasmConnection(prev)) {
302 1 : logger.warn('refusing to delete the connection to the embedded Hyper', { connectionId }, LOG_CTX);
303 1 : return reg;
304 1 : }
305 1 : if (action.type == SWITCH_CONNECTOR_TYPE && sessionIds.length > 1) {
306 1 : logger.warn('refusing to switch the type of a shared connection', { connectionId }, LOG_CTX);
307 1 : return reg;
308 1 : }
309 1 : const next = reduceConnectionState(prev, action, logger);
310 :
311 1 : if (action.type == DELETE_CONNECTION) {
312 1 : reg.connectionsByType[connectorType] = reg.connectionsByType[connectorType].filter(id => id != connectionId);
313 1 : reg.connections.delete(connectionId);
314 1 : if (prev.active) {
315 1 : void storageWriter.write(groupConnectionEntryWrites(connectionId), { type: DELETE_CONNECTION_ENTRY, value: connectionId });
316 : }
317 1 : return { ...reg };
318 1 : }
319 1 : reg.connections.set(connectionId, next);
320 : // Update type index when connector type changes
321 1 : if (action.type == SWITCH_CONNECTOR_TYPE && next.connectorInfo.connectorType !== connectorType) {
322 1 : reg.connectionsByType[connectorType] = reg.connectionsByType[connectorType].filter(id => id != connectionId);
323 1 : reg.connectionsByType[next.connectorInfo.connectorType].push(connectionId);
324 1 : }
325 : // Only connections that sessions reference or that were activated are persisted.
326 : // Saving writes the committed params by itself and awaits the writes.
327 1 : if (sessionIds.length === 0 && !next.active) {
328 1 : return { ...reg };
329 : }
330 1 : if (action.type !== COMMIT_CONNECTION_PARAMS) {
331 1 : if (didConnectionParamsChange(prev, next) || (!prev.active && next.active)) {
332 : // Saved params are replicated into every session that references the connection
333 1 : scheduleConnectionParamsWrites(references, next, storageWriter);
334 1 : } else if (didPersistedConnectionChange(prev, next)) {
335 : // Drafts, names and the caching choice only live in the app manifest
336 1 : void storageWriter.write(
337 1 : groupConnectionEntryWrites(connectionId),
338 1 : { type: WRITE_CONNECTION_ENTRY, value: connectionEntryFromState(next) },
339 : DEBOUNCE_DURATION_CONNECTION_DRAFT_WRITE,
340 1 : );
341 : }
342 1 : }
343 1 : if (!next.active) {
344 1 : return { ...reg };
345 : }
346 : // The catalog of the connection is written into the app storage, sessions don't keep a copy.
347 : // The relations of a connection whose database doesn't outlive the app are never written.
348 1 : const durable = connectionCatalogIsDurable(next.params);
349 1 : if (action.type === CATALOG_UPDATE_SUCCEEDED || action.type === CATALOG_UPDATE_PARTIALLY_SUCCEEDED) {
350 1 : const relationsSql = next.catalogRelationScript.toString();
351 1 : const functionsSql = next.catalogFunctionScript.toString();
352 1 : if (durable) {
353 1 : void storageWriter.write(
354 1 : groupConnectionFileWrites(connectionId, STORAGE_CATALOG_RELATIONS),
355 1 : { type: WRITE_CONNECTION_FILE, value: [connectionId, STORAGE_CATALOG_RELATIONS, relationsSql] },
356 : DEBOUNCE_DURATION_SESSION_WRITE,
357 1 : );
358 : }
359 1 : void storageWriter.write(
360 1 : groupConnectionFileWrites(connectionId, STORAGE_CATALOG_FUNCTIONS),
361 1 : { type: WRITE_CONNECTION_FILE, value: [connectionId, STORAGE_CATALOG_FUNCTIONS, functionsSql] },
362 : DEBOUNCE_DURATION_SESSION_WRITE,
363 1 : );
364 1 : }
365 : // Executed statements record catalog changes, a refresh clears them
366 1 : if (durable && next.catalogChangesSql !== prev.catalogChangesSql) {
367 1 : void storageWriter.write(
368 1 : groupConnectionFileWrites(connectionId, STORAGE_CATALOG_CHANGES),
369 1 : { type: WRITE_CONNECTION_FILE, value: [connectionId, STORAGE_CATALOG_CHANGES, next.catalogChangesSql] },
370 : DEBOUNCE_DURATION_SESSION_WRITE,
371 1 : );
372 : }
373 1 : return { ...reg };
374 1 : }
375 :
376 : /// Dispatch actions to connections without subscribing to registry changes.
377 : /// The dispatches of all components are applied in order through the queue of the registry.
378 1 : export function useConnectionDispatch(): DynamicConnectionDispatch {
379 1 : const queue = React.useContext(CONNECTION_REGISTRY_QUEUE_CTX)!;
380 1 : const references = useConnectionReferences();
381 1 : const storageWriter = useStorageWriter();
382 1 : const logger = useLogger();
383 1 : return React.useCallback((connectionId: string | null, action: ConnectionAction) => {
384 : // No id provided? Then do nothing.
385 1 : if (connectionId == null) {
386 1 : return;
387 : }
388 : // Query execution may happen in a worker or remotely, but Arrow batch decoding and these
389 : // state updates happen on the renderer thread. A fast result stream can therefore enqueue
390 : // a microtask cascade that repeatedly rerenders the session before the browser gets a
391 : // frame to paint. Publish only the latest streamed progress once per frame; lifecycle
392 : // actions stay immediate and flush queued stream updates first to preserve action order.
393 1 : const waitForFrame = action.type === QUERY_PROGRESS_UPDATED || action.type === QUERY_RECEIVED_BATCH;
394 1 : queue.push(reg => applyConnectionAction(reg, references, connectionId, action, storageWriter, logger), waitForFrame);
395 1 : }, [queue, references, storageWriter, logger]);
396 1 : }
397 :
398 : /// The connections that the scripts of a session use
399 1 : export function useSessionConnections(sessionId: string | null): ConnectionState[] | null {
400 1 : const [registry] = useConnectionRegistry();
401 1 : const references = useConnectionReferences();
402 : // Re-render when the session uses other connections
403 1 : useConnectionReferencesSelector(references => sessionId == null ? '' : references.connectionsOf(sessionId).join('\n'));
404 : // The list keeps its identity while the connections are unchanged, consumers depend on it in effects
405 1 : const resolved = resolveSessionConnections(registry, references, sessionId);
406 1 : const latest = React.useRef(resolved);
407 1 : const same = latest.current === resolved || (latest.current != null && resolved != null
408 1 : && latest.current.length === resolved.length && latest.current.every((connection, i) => connection === resolved[i]));
409 1 : if (!same) latest.current = resolved;
410 1 : return latest.current;
411 1 : }
412 :
413 1 : export function useConnectionById(connectionId: string | null): [ConnectionState | null, ConnectionDispatch] {
414 1 : const [registry, dispatch] = useDynamicConnectionDispatch();
415 1 : const capturingDispatch = React.useCallback((action: ConnectionAction) => dispatch(connectionId, action), [connectionId, dispatch]);
416 1 : return [connectionId == null ? null : registry.connections.get(connectionId) ?? null, capturingDispatch];
417 1 : }
418 :
419 : /// Editing dispatches immediately; the registry debounces only the durable manifest write.
420 1 : export function useConnectionDraft(connectionId: string | null) {
421 1 : const [database, dispatch] = useConnectionById(connectionId);
422 1 : const setDraft = React.useCallback((params: ConnectionParams) => {
423 1 : dispatch({ type: SET_CONNECTION_DRAFT, value: params });
424 1 : }, [dispatch]);
425 1 : const discard = React.useCallback(() => {
426 0 : dispatch({ type: DISCARD_CONNECTION_DRAFT, value: null });
427 1 : }, [dispatch]);
428 1 : return { params: database?.params ?? null, paramsDraft: database?.paramsDraft, setDraft, discard };
429 1 : }
430 :
431 : /// Forms write from user actions, never from hydration effects (which could resurrect a draft).
432 1 : export function useConnectionDraftForm<T>(
433 1 : connectionId: string | null,
434 1 : decode: (params: ConnectionParams | null) => T,
435 1 : encode: (form: T) => ConnectionParams,
436 1 : ): [T, React.Dispatch<React.SetStateAction<T>>] {
437 1 : const { params, paramsDraft, setDraft } = useConnectionDraft(connectionId);
438 1 : const source = paramsDraft ?? params;
439 1 : const [form, setForm] = React.useState(() => decode(source));
440 1 : const sourceRef = React.useRef(source);
441 1 : const formRef = React.useRef(form);
442 1 : const databaseIdRef = React.useRef(connectionId);
443 1 : const editedParamsRef = React.useRef<ConnectionParams | undefined>(undefined);
444 1 : if (databaseIdRef.current !== connectionId || sourceRef.current !== source) {
445 1 : const ownEdit = databaseIdRef.current === connectionId && paramsDraft != null
446 1 : && connectionParamsEqual(editedParamsRef.current, paramsDraft);
447 1 : databaseIdRef.current = connectionId;
448 1 : sourceRef.current = source;
449 1 : if (!ownEdit) {
450 1 : editedParamsRef.current = undefined;
451 1 : formRef.current = decode(source);
452 1 : setForm(formRef.current);
453 1 : }
454 1 : }
455 1 : const update = (value: React.SetStateAction<T>) => {
456 1 : const next = typeof value === 'function' ? (value as (prev: T) => T)(formRef.current) : value;
457 1 : formRef.current = next;
458 1 : setForm(next);
459 1 : const encoded = encode(next);
460 1 : editedParamsRef.current = encoded;
461 1 : setDraft(encoded);
462 1 : };
463 1 : return [formRef.current, update];
464 1 : }
|