feat(cli): consistent connection setup recovery and build-time gate (#257)

* feat(cli): block context build when a required connection fails its live test

A context build can take several minutes, so a connection that is
unreachable or misconfigured should stop the build up front instead of
failing partway through. Before the build starts, run a live connection
test for every primary- and context-source connection the build depends
on.

Each test's output is captured in a discarded buffer so raw error text
(and database paths) never reach the user; failures are surfaced only by
connection id and connector type, with a pointer to `ktx connection test
<id>` for the underlying error.

- Interactive setup lets the user fix the connection and retry without
  restarting, re-resolving targets so an added/removed/reconfigured
  connection is honored.
- `--no-input` exits non-zero and writes a failed context state with a
  failureReason, so scripts stop early and setup never reads as ready.

Extract the buffered command IO helper out of setup-databases into
src/io/buffered-command-io.ts so both call sites share one implementation.

* feat(cli): use recovery primitive for database setup

* feat(cli): use recovery primitive for source setup

* docs: document setup connection recovery

* fix(cli): close database recovery gaps

* fix(cli): target failing project in gate hint and preserve missing-input

Address two review findings on the connection-recovery work:

- The connection-gate failure hint emitted `ktx connection test <id>` with no
  --project-dir, so a setup run started with `--project-dir ./analytics` pointed
  users at cwd/KTX_PROJECT_DIR instead of the project that just failed. Emit the
  resolved project dir, matching the contextBuildCommands convention.

- The non-interactive database configure path returned `cancelled`, which the
  recovery primitive collapses to `failed`. Sibling paths still report
  `missing-input` for absent flags, so incomplete-flag runs were
  indistinguishable from real connection failures. The database wrapper now
  tracks the configure missing-input signal and restores the `missing-input`
  step status; the shared primitive keeps its four outcomes.
This commit is contained in:
Andrey Avtomonov 2026-06-03 13:08:46 +02:00 committed by GitHub
parent f5dea9a089
commit ce1516b357
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1531 additions and 354 deletions

View file

@ -22,6 +22,13 @@ import {
writePrefixedLines,
} from './clack.js';
import { runKtxConnection } from './connection.js';
import { createBufferedCommandIo } from './io/buffered-command-io.js';
import {
runConnectionSetupWithRecovery,
type ConfigureResult,
type RecoveryOutcome,
type ValidateResult,
} from './connection-recovery.js';
import {
pickDatabaseScope as defaultPickDatabaseScope,
type DatabaseScopePickResult,
@ -227,7 +234,6 @@ const SCOPE_DISCOVERY_SPECS: Partial<Record<KtxSetupDatabaseDriver, ScopeDiscove
};
type UrlDriverType = Extract<KtxSetupDatabaseDriver, 'postgres' | 'mysql' | 'clickhouse' | 'sqlserver'>;
type ConnectionSetupStatus = 'ready' | 'back' | 'failed' | 'failed-query-history-unavailable';
const DRIVER_CONNECTION_DEFAULTS: Record<UrlDriverType, { port: string }> = {
postgres: { port: '5432' },
@ -994,35 +1000,6 @@ async function defaultScanConnection(projectDir: string, connectionId: string, i
);
}
interface BufferedCommandIo extends KtxCliIo {
stdoutText(): string;
stderrText(): string;
}
function createBufferedCommandIo(): BufferedCommandIo {
let stdout = '';
let stderr = '';
return {
stdout: {
isTTY: false,
write(chunk: string) {
stdout += chunk;
},
},
stderr: {
write(chunk: string) {
stderr += chunk;
},
},
stdoutText() {
return stdout;
},
stderrText() {
return stderr;
},
};
}
function envWithCurrentNodeFirst(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
return {
...env,
@ -1203,6 +1180,31 @@ async function disableConnectionQueryHistory(projectDir: string, connectionId: s
});
}
function okValidateResult(): ValidateResult {
return { status: 'ok' };
}
function backValidateResult(): ValidateResult {
return { status: 'back' };
}
function failedValidateResult(): ValidateResult {
return { status: 'failed' };
}
function queryHistoryUnavailableResult(projectDir: string, connectionId: string): ValidateResult {
return {
status: 'failed',
extraActions: [
{
value: 'disable-query-history',
label: 'Disable query history and retry',
run: () => disableConnectionQueryHistory(projectDir, connectionId),
},
],
};
}
async function createConnectionConfigRollback(projectDir: string, connectionId: string): Promise<() => Promise<void>> {
const project = await loadKtxProject({ projectDir });
const previousConnection = project.config.connections[connectionId];
@ -1330,11 +1332,11 @@ async function maybeConfigureDatabaseScope(input: {
io: KtxCliIo;
prompts: KtxSetupDatabasesPromptAdapter;
forcePrompt?: boolean;
}): Promise<ConnectionSetupStatus> {
}): Promise<ValidateResult> {
const project = await loadKtxProject({ projectDir: input.projectDir });
const connection = project.config.connections[input.connectionId];
const driver = normalizeDriver(connection?.driver);
if (!driver || driver === 'sqlite') return 'ready';
if (!driver || driver === 'sqlite') return okValidateResult();
const spec = SCOPE_DISCOVERY_SPECS[driver];
const existingTables = connection?.enabled_tables;
@ -1343,7 +1345,7 @@ async function maybeConfigureDatabaseScope(input: {
const hasExistingScope = !spec || existingScope.length > 0;
if (hasExistingTables && hasExistingScope && input.forcePrompt !== true) {
return 'ready';
return okValidateResult();
}
const cliSchemas = input.args.databaseSchemas;
@ -1361,7 +1363,7 @@ async function maybeConfigureDatabaseScope(input: {
input.io.stderr.write(
`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`,
);
return 'ready';
return okValidateResult();
}
}
if (scopeToWrite.length > 0) {
@ -1377,7 +1379,7 @@ async function maybeConfigureDatabaseScope(input: {
]);
}
}
return 'ready';
return okValidateResult();
}
if (spec && cliSchemas.length > 0) {
@ -1413,7 +1415,7 @@ async function maybeConfigureDatabaseScope(input: {
connectionId: input.connectionId,
spec,
});
if (typed === undefined) return 'back';
if (typed === undefined) return backValidateResult();
effectiveCliSchemas = typed;
listedSchemas = typed;
if (typed.length > 0) {
@ -1428,7 +1430,7 @@ async function maybeConfigureDatabaseScope(input: {
}
const schemas = unique(listedSchemas);
if (spec && schemas.length === 0) {
return 'ready';
return okValidateResult();
}
const schemaSuggestion =
effectiveCliSchemas.length > 0
@ -1465,10 +1467,10 @@ async function maybeConfigureDatabaseScope(input: {
? `Could not discover tables for ${input.connectionId}; edit was not saved. ${detail}`
: `Could not discover tables for ${input.connectionId}; continuing without table filter. ${detail}`,
);
return input.forcePrompt === true ? 'failed' : 'ready';
return input.forcePrompt === true ? failedValidateResult() : okValidateResult();
}
if (pickResult.kind === 'back') {
return 'back';
return backValidateResult();
}
const enabledTables = pickResult.enabledTables;
const activeSchemas = pickResult.activeSchemas;
@ -1483,7 +1485,7 @@ async function maybeConfigureDatabaseScope(input: {
}
const refreshedProject = await loadKtxProject({ projectDir: input.projectDir });
const currentConnection = refreshedProject.config.connections[input.connectionId];
if (!currentConnection) return 'ready';
if (!currentConnection) return okValidateResult();
await writeConnectionConfig({
projectDir: input.projectDir,
connectionId: input.connectionId,
@ -1500,7 +1502,7 @@ async function maybeConfigureDatabaseScope(input: {
writeSetupSection(input.io, `Tables enabled for ${input.connectionId}`, [
`${enabledTables.length} tables enabled`,
]);
return 'ready';
return okValidateResult();
}
async function ensureHistoricSqlIngestDefaults(projectDir: string): Promise<void> {
@ -1628,7 +1630,7 @@ async function validateAndScanConnection(input: {
args: KtxSetupDatabasesArgs;
prompts: KtxSetupDatabasesPromptAdapter;
forceScopeAndTables?: boolean;
}): Promise<ConnectionSetupStatus> {
}): Promise<ValidateResult> {
const testConnection = input.deps.testConnection ?? defaultTestConnection;
const scanConnection = input.deps.scanConnection ?? defaultScanConnection;
const project = await loadKtxProject({ projectDir: input.projectDir });
@ -1642,7 +1644,7 @@ async function validateAndScanConnection(input: {
(chunk) => input.io.stderr.write(chunk),
`Connection test failed for ${input.connectionId}.`,
);
return 'failed';
return failedValidateResult();
}
const testOutput = testIo.stdoutText();
const outputDriver = normalizeDriver(readOutputValue(testOutput, 'Driver'));
@ -1651,7 +1653,7 @@ async function validateAndScanConnection(input: {
writeSetupSection(input.io, `Testing ${input.connectionId}`, testLines);
const scopeStatus = await maybeConfigureDatabaseScope({ ...input, forcePrompt: input.forceScopeAndTables });
if (scopeStatus !== 'ready') {
if (scopeStatus.status !== 'ok') {
return scopeStatus;
}
@ -1712,7 +1714,9 @@ async function validateAndScanConnection(input: {
);
}
if (scanCode !== 0) {
return queryHistoryAvailable ? 'failed' : 'failed-query-history-unavailable';
return queryHistoryAvailable
? failedValidateResult()
: queryHistoryUnavailableResult(input.projectDir, input.connectionId);
}
}
const scanOutput = scanIo.stdoutText();
@ -1724,7 +1728,7 @@ async function validateAndScanConnection(input: {
writeSetupSection(input.io, 'Database ready', [
`${input.connectionId} · ${driverDisplay} · schema context complete`,
]);
return 'ready';
return okValidateResult();
}
async function chooseDrivers(
@ -1847,6 +1851,149 @@ async function choosePrimarySourceToEdit(input: {
return choice === 'back' ? 'back' : choice;
}
async function configureDatabaseConnection(input: {
projectDir: string;
connectionId: string;
driver: KtxSetupDatabaseDriver;
args: KtxSetupDatabasesArgs;
prompts: KtxSetupDatabasesPromptAdapter;
io: KtxCliIo;
canReturnToDriverSelection: boolean;
editBaseline?: KtxProjectConnectionConfig;
}): Promise<ConfigureResult> {
const project = await loadKtxProject({ projectDir: input.projectDir });
const latestConnection = project.config.connections[input.connectionId];
let connection = await buildConnectionConfig({
driver: input.driver,
connectionId: input.connectionId,
args: input.args,
prompts: input.prompts,
existingConnection: latestConnection,
});
while (!connection && input.args.inputMode !== 'disabled') {
const action = await input.prompts.select(
missingConnectionDetailsPrompt(driverLabel(input.driver), input.canReturnToDriverSelection),
);
if (action === 'back') {
return 'back';
}
connection = await buildConnectionConfig({
driver: input.driver,
connectionId: input.connectionId,
args: input.args,
prompts: input.prompts,
existingConnection: latestConnection,
});
}
if (connection === 'back') {
return 'back';
}
if (!connection) {
input.io.stderr.write(`Missing connection details for ${driverLabel(input.driver)}.\n`);
return 'cancelled';
}
const withHistoricSql = await maybeApplyHistoricSqlConfig({
connection,
driver: input.driver,
args: input.args,
prompts: input.prompts,
});
if (withHistoricSql === 'back') {
return 'back';
}
await writeConnectionConfig({
projectDir: input.projectDir,
connectionId: input.connectionId,
connection: input.editBaseline
? withExistingPrimaryEditPromptDefaults({
previous: input.editBaseline,
next: withHistoricSql,
driver: input.driver,
})
: withHistoricSql,
io: input.io,
});
return 'configured';
}
async function runDatabaseConnectionSetupWithRecovery(input: {
projectDir: string;
connectionId: string;
driver: KtxSetupDatabaseDriver;
args: KtxSetupDatabasesArgs;
prompts: KtxSetupDatabasesPromptAdapter;
io: KtxCliIo;
deps: KtxSetupDatabasesDeps;
canReturnToDriverSelection: boolean;
allowSkip: boolean;
interactive?: boolean;
forceScopeAndTables?: boolean;
editBaseline?: KtxProjectConnectionConfig;
reuseExistingOnFirstConfigure?: boolean;
}): Promise<RecoveryOutcome | 'missing-input'> {
let configureCalls = 0;
// `configureDatabaseConnection` returns 'cancelled' only when required
// connection details are absent in non-interactive mode. The recovery
// primitive collapses that into 'failed', so we track it here to restore the
// distinct 'missing-input' outcome the surrounding step reports for
// incomplete flags (vs. a real connection/probe failure).
let sawMissingInput = false;
const outcome = await runConnectionSetupWithRecovery({
label: input.connectionId,
interactive: input.interactive ?? input.args.inputMode !== 'disabled',
allowSkip: input.allowSkip,
io: input.io,
prompts: input.prompts,
snapshot: () => createConnectionConfigRollback(input.projectDir, input.connectionId),
configure: async () => {
configureCalls += 1;
if (input.reuseExistingOnFirstConfigure && configureCalls === 1) {
const historicSqlResult = await applyHistoricSqlConfigToExistingConnection({
projectDir: input.projectDir,
connectionId: input.connectionId,
args: input.args,
prompts: input.prompts,
});
return historicSqlResult === 'back' ? 'back' : 'configured';
}
const configured = await configureDatabaseConnection({
projectDir: input.projectDir,
connectionId: input.connectionId,
driver: input.driver,
args: input.args,
prompts: input.prompts,
io: input.io,
canReturnToDriverSelection: input.canReturnToDriverSelection,
editBaseline: input.editBaseline,
});
if (configured === 'cancelled') {
sawMissingInput = true;
}
return configured;
},
validate: () =>
validateAndScanConnection({
projectDir: input.projectDir,
connectionId: input.connectionId,
io: input.io,
deps: input.deps,
args: input.args,
prompts: input.prompts,
forceScopeAndTables: input.forceScopeAndTables,
}),
});
if (outcome === 'failed' && sawMissingInput) {
return 'missing-input';
}
return outcome;
}
async function runPrimarySourceFullEdit(input: {
projectDir: string;
connectionId: string;
@ -1854,7 +2001,7 @@ async function runPrimarySourceFullEdit(input: {
prompts: KtxSetupDatabasesPromptAdapter;
io: KtxCliIo;
deps: KtxSetupDatabasesDeps;
}): Promise<'ready' | 'back' | 'failed'> {
}): Promise<'ready' | 'back' | 'failed' | 'missing-input'> {
const project = await loadKtxProject({ projectDir: input.projectDir });
const existing = project.config.connections[input.connectionId];
const driver = normalizeDriver(existing?.driver);
@ -1866,59 +2013,21 @@ async function runPrimarySourceFullEdit(input: {
return 'failed';
}
const rollback = await createConnectionConfigRollback(input.projectDir, input.connectionId);
const replacement = await buildConnectionConfig({
driver,
const outcome = await runDatabaseConnectionSetupWithRecovery({
projectDir: input.projectDir,
connectionId: input.connectionId,
args: input.args,
prompts: input.prompts,
existingConnection: existing,
});
if (replacement === 'back') {
await rollback();
return 'back';
}
if (!replacement) {
await rollback();
return 'failed';
}
const withHistoricSql = await maybeApplyHistoricSqlConfig({
connection: replacement,
driver,
args: input.args,
prompts: input.prompts,
});
if (withHistoricSql === 'back') {
await rollback();
return 'back';
}
await writeConnectionConfig({
projectDir: input.projectDir,
connectionId: input.connectionId,
connection: withExistingPrimaryEditPromptDefaults({
previous: existing,
next: withHistoricSql,
driver,
}),
io: input.io,
});
const validated = await validateAndScanConnection({
projectDir: input.projectDir,
connectionId: input.connectionId,
io: input.io,
deps: input.deps,
args: input.args,
prompts: input.prompts,
canReturnToDriverSelection: true,
allowSkip: false,
forceScopeAndTables: true,
editBaseline: existing,
});
if (validated !== 'ready') {
await rollback();
return validated === 'failed-query-history-unavailable' ? 'failed' : validated;
}
return 'ready';
return outcome === 'skip' ? 'back' : outcome;
}
export async function runKtxSetupDatabasesStep(
@ -1936,28 +2045,37 @@ export async function runKtxSetupDatabasesStep(
if (args.databaseConnectionIds && args.databaseConnectionIds.length > 0) {
const selectedConnectionIds: string[] = [];
for (const connectionId of unique(args.databaseConnectionIds)) {
const historicSqlResult = await applyHistoricSqlConfigToExistingConnection({
projectDir: args.projectDir,
connectionId,
args,
prompts,
});
if (historicSqlResult === 'back') return { status: 'back', projectDir: args.projectDir };
const setupStatus = await validateAndScanConnection({
projectDir: args.projectDir,
connectionId,
io,
deps,
args,
prompts,
});
if (setupStatus === 'back') {
return { status: 'back', projectDir: args.projectDir };
}
if (setupStatus === 'failed') {
const project = await loadKtxProject({ projectDir: args.projectDir });
const driver = normalizeDriver(project.config.connections[connectionId]?.driver);
if (!driver) {
writePrefixedLines((chunk) => io.stderr.write(chunk), `Connection "${connectionId}" is not configured.`);
return { status: 'failed', projectDir: args.projectDir };
}
selectedConnectionIds.push(connectionId);
const setupOutcome = await runDatabaseConnectionSetupWithRecovery({
projectDir: args.projectDir,
connectionId,
driver,
args,
prompts,
io,
deps,
canReturnToDriverSelection: false,
allowSkip: false,
interactive: false,
reuseExistingOnFirstConfigure: true,
});
if (setupOutcome === 'back') {
return { status: 'back', projectDir: args.projectDir };
}
if (setupOutcome === 'missing-input') {
return { status: 'missing-input', projectDir: args.projectDir };
}
if (setupOutcome === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
}
if (setupOutcome === 'ready') {
selectedConnectionIds.push(connectionId);
}
}
await markDatabasesComplete(args.projectDir, selectedConnectionIds);
return { status: 'ready', projectDir: args.projectDir, connectionIds: selectedConnectionIds };
@ -2009,6 +2127,9 @@ export async function runKtxSetupDatabasesStep(
showConfiguredPrimaryMenu = true;
continue;
}
if (editResult === 'missing-input') {
return { status: 'missing-input', projectDir: args.projectDir };
}
if (editResult === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
}
@ -2064,7 +2185,6 @@ export async function runKtxSetupDatabasesStep(
return { status: 'missing-input', projectDir: args.projectDir };
}
let connectionAlreadyValidated = false;
if (connectionChoice.kind === 'edit') {
const editResult = await runPrimarySourceFullEdit({
projectDir: args.projectDir,
@ -2079,176 +2199,41 @@ export async function runKtxSetupDatabasesStep(
returnToDriverSelection = true;
break;
}
if (editResult === 'missing-input') {
return { status: 'missing-input', projectDir: args.projectDir };
}
if (editResult === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
}
connectionAlreadyValidated = true;
} else if (connectionChoice.kind === 'new') {
let connection = await buildConnectionConfig({
driver,
} else {
const setupOutcome = await runDatabaseConnectionSetupWithRecovery({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
driver,
args,
prompts,
io,
deps,
canReturnToDriverSelection,
allowSkip: true,
reuseExistingOnFirstConfigure: connectionChoice.kind === 'existing',
});
if (connection === 'back') {
if (setupOutcome === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
while (!connection && args.inputMode !== 'disabled') {
const label = driverLabel(driver);
const action = await prompts.select(missingConnectionDetailsPrompt(label, canReturnToDriverSelection));
if (action === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
connection = await buildConnectionConfig({
driver,
connectionId: connectionChoice.connectionId,
args,
prompts,
});
if (connection === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
}
if (returnToDriverSelection) {
break;
}
if (connection === 'back') {
break;
}
if (!connection) {
io.stderr.write(`Missing connection details for ${driverLabel(driver)}.\n`);
if (setupOutcome === 'missing-input') {
return { status: 'missing-input', projectDir: args.projectDir };
}
const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
if (withHistoricSql === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
if (setupOutcome === 'failed') {
return { status: 'failed', projectDir: args.projectDir };
}
await writeConnectionConfig({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
connection: withHistoricSql,
io,
});
} else {
const existing = project.config.connections[connectionChoice.connectionId];
const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection: existing, driver, args, prompts });
if (withHistoricSql === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
await writeConnectionConfig({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
connection: withHistoricSql,
io,
});
}
let connectionSkipped = false;
let setupStatus: ConnectionSetupStatus = connectionAlreadyValidated
? 'ready'
: await validateAndScanConnection({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
io,
deps,
args,
prompts,
});
while (!connectionAlreadyValidated && setupStatus !== 'ready') {
if (setupStatus === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
if (args.inputMode === 'disabled') return { status: 'failed', projectDir: args.projectDir };
const failureOptions = [
{ value: 'retry', label: 'Retry connection test' },
{ value: 're-enter', label: 'Re-enter connection details' },
...(setupStatus === 'failed-query-history-unavailable'
? [{ value: 'disable-query-history', label: 'Disable query history and retry' }]
: []),
{ value: 'skip', label: 'Skip this database' },
{ value: 'back', label: 'Back' },
];
const action = await prompts.select({
message: `Database setup failed for ${connectionChoice.connectionId}`,
options: failureOptions,
});
if (action === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
if (action === 'skip') {
connectionSkipped = true;
break;
}
if (action === 'retry') {
setupStatus = await validateAndScanConnection({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
io,
deps,
args,
prompts,
});
} else if (action === 'disable-query-history') {
await disableConnectionQueryHistory(args.projectDir, connectionChoice.connectionId);
setupStatus = await validateAndScanConnection({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
io,
deps,
args,
prompts,
});
} else if (action === 're-enter') {
const connection = await buildConnectionConfig({
driver,
connectionId: connectionChoice.connectionId,
args,
prompts,
});
if (connection === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
if (!connection) continue;
const withHistoricSql = await maybeApplyHistoricSqlConfig({ connection, driver, args, prompts });
if (withHistoricSql === 'back') {
if (!canReturnToDriverSelection) return { status: 'back', projectDir: args.projectDir };
returnToDriverSelection = true;
break;
}
await writeConnectionConfig({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
connection: withHistoricSql,
io,
});
setupStatus = await validateAndScanConnection({
projectDir: args.projectDir,
connectionId: connectionChoice.connectionId,
io,
deps,
args,
prompts,
});
if (setupOutcome === 'skip') {
continue;
}
}
if (returnToDriverSelection) break;
if (connectionSkipped) continue;
pushUniqueConnectionId(selectedConnectionIds, connectionChoice.connectionId);
}