@databricks/appkit 0.61.1 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js.map +1 -1
- package/dist/agents/supervisor-api.d.ts.map +1 -1
- package/dist/agents/supervisor-api.js.map +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/memory.js.map +1 -1
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/cli/commands/codemod/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/index.js.map +1 -1
- package/dist/cli/commands/doctor/report.js.map +1 -1
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
- package/dist/cli/commands/doctor/run.js.map +1 -1
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/client.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js.map +1 -1
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/cli/commands/registry/index.js.map +1 -1
- package/dist/cli/commands/registry/info.js.map +1 -1
- package/dist/cli/commands/registry/list.js.map +1 -1
- package/dist/cli/commands/registry/requirements.js.map +1 -1
- package/dist/cli/commands/registry/server-register.js.map +1 -1
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/lakebase/index.d.ts.map +1 -1
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -1
- package/dist/connectors/lakebase/pool-manager.js.map +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/connectors/mcp/client.d.ts.map +1 -1
- package/dist/connectors/mcp/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/context/client-options.js.map +1 -1
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +1 -1
- package/dist/context/service-context.d.ts +55 -3
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js.map +1 -1
- package/dist/core/agent/build-toolkit.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/toolkit-resolver.js.map +1 -1
- package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
- package/dist/core/agent/tools/define-tool.js.map +1 -1
- package/dist/core/agent/tools/tool.d.ts.map +1 -1
- package/dist/core/agent/tools/tool.js.map +1 -1
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js.map +1 -1
- package/dist/core/plugin-context.d.ts +13 -0
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +12 -1
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/configuration.d.ts.map +1 -1
- package/dist/errors/configuration.js.map +1 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/logging/wide-event-emitter.js.map +1 -1
- package/dist/plugin/dev-reader.d.ts.map +1 -1
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +2 -2
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/event-translator.js.map +1 -1
- package/dist/plugins/agents/thread-store.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/mv/cache.js.map +1 -1
- package/dist/plugins/analytics/mv/constants.js.map +1 -1
- package/dist/plugins/analytics/mv/formatters.js.map +1 -1
- package/dist/plugins/analytics/mv/metadata.js.map +1 -1
- package/dist/plugins/analytics/mv/registry.js.map +1 -1
- package/dist/plugins/analytics/mv/schemas.js.map +1 -1
- package/dist/plugins/analytics/query.js.map +1 -1
- package/dist/plugins/analytics/result-delivery.js.map +1 -1
- package/dist/plugins/files/helpers.js.map +1 -1
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/jobs/plugin.d.ts.map +1 -1
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/jobs/types.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/lakebase/types.d.ts.map +1 -1
- package/dist/plugins/server/base-server.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/react-source-loc-vite-plugin.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/plugins/serving/schema-filter.js.map +1 -1
- package/dist/plugins/serving/serving.d.ts.map +1 -1
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/plugins/ui-variants/index.js.map +1 -1
- package/dist/registry/manifest-loader.d.ts.map +1 -1
- package/dist/registry/manifest-loader.js.map +1 -1
- package/dist/registry/resource-registry.d.ts.map +1 -1
- package/dist/registry/resource-registry.js.map +1 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/types.js.map +1 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -1
- package/dist/shared/src/schemas/metric-source.js.map +1 -1
- package/dist/shared/src/sse/analytics.js.map +1 -1
- package/dist/shared/src/workspace-client/legacy.js.map +1 -1
- package/dist/shared/src/workspace-client/types.d.ts.map +1 -1
- package/dist/stream/sse-writer.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/instrumentations.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/telemetry/telemetry-provider.js.map +1 -1
- package/dist/telemetry/trace-sampler.js.map +1 -1
- package/dist/testing/expect-stream.d.ts +111 -0
- package/dist/testing/expect-stream.d.ts.map +1 -0
- package/dist/testing/expect-stream.js +154 -0
- package/dist/testing/expect-stream.js.map +1 -0
- package/dist/testing/fixtures.d.ts +257 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +385 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/test-plugin-context.d.ts +158 -0
- package/dist/testing/test-plugin-context.d.ts.map +1 -0
- package/dist/testing/test-plugin-context.js +139 -0
- package/dist/testing/test-plugin-context.js.map +1 -0
- package/dist/type-generator/cache.js.map +1 -1
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/cache.js.map +1 -1
- package/dist/type-generator/serving/generator.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/serving/vite-plugin.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/workspace-client/legacy.js.map +1 -1
- package/dist/workspace-client/types.d.ts.map +1 -1
- package/docs/plugins/testing.md +245 -0
- package/llms.txt +1 -0
- package/package.json +15 -2
- package/sbom.cdx.json +1 -1
- package/skills/appkit-ui-variants/SKILL.md +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/files/client.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport type { TelemetryOptions } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport type {\n DirectoryEntry,\n DownloadResponse,\n FileMetadata,\n FilePreview,\n} from \"../../plugins/files/types\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport { ApiError, type WorkspaceClient } from \"../../workspace-client\";\nimport {\n contentTypeFromPath,\n FILES_MAX_READ_SIZE,\n isTextContentType,\n} from \"./defaults\";\n\nconst logger = createLogger(\"connectors:files\");\n\n/**\n * Ambient span-attribute propagation for `FilesConnector.traced()`.\n *\n * Callers (e.g. the plugin's `_withAuthModeAttributes` wrapper) set extra\n * span attributes here via `runWithFilesSpanAttributes(attrs, fn)`. The\n * connector's `traced()` decorator reads them and merges them into the\n * span it creates around the SDK call. This lets the plugin tag spans with\n * `files.auth_mode` without opening a duplicate `files.<op>` span.\n *\n * AsyncLocalStorage is used so concurrent requests don't see each other's\n * attributes. Outside an active scope, `getStore()` returns `undefined` and\n * the connector falls back to the static attribute set.\n */\nconst filesSpanAttributesStorage = new AsyncLocalStorage<\n Record<string, string>\n>();\n\n/**\n * Run `fn` with the supplied attributes attached to whatever span the\n * `FilesConnector` opens for its SDK call. Used to propagate request-scoped\n * attributes (e.g. `files.auth_mode`) onto the connector's span without\n * opening a parent span — avoids the 2x span allocation that\n * `startActiveSpan` parented otherwise.\n */\nexport function runWithFilesSpanAttributes<T>(\n attributes: Record<string, string>,\n fn: () => Promise<T>,\n): Promise<T> {\n return filesSpanAttributesStorage.run(attributes, fn);\n}\n\ninterface FilesConnectorConfig {\n defaultVolume?: string;\n timeout?: number;\n telemetry?: TelemetryOptions;\n customContentTypes?: Record<string, string>;\n}\n\nexport class FilesConnector {\n private readonly name = \"files\";\n private defaultVolume: string | undefined;\n private readonly customContentTypes: Record<string, string> | undefined;\n\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n operationCount: Counter;\n operationDuration: Histogram;\n };\n\n constructor(config: FilesConnectorConfig) {\n this.defaultVolume = config.defaultVolume;\n this.customContentTypes = config.customContentTypes;\n\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.telemetryMetrics = {\n operationCount: this.telemetry\n .getMeter()\n .createCounter(\"files.operation.count\", {\n description: \"Total number of file operations\",\n unit: \"1\",\n }),\n operationDuration: this.telemetry\n .getMeter()\n .createHistogram(\"files.operation.duration\", {\n description: \"Duration of file operations\",\n unit: \"ms\",\n }),\n };\n }\n\n resolvePath(filePath: string): string {\n if (filePath.length > 4096) {\n throw new Error(\n `Path exceeds maximum length of 4096 characters (got ${filePath.length}).`,\n );\n }\n if (filePath.includes(\"\\0\")) {\n throw new Error(\"Path must not contain null bytes.\");\n }\n\n const segments = filePath.split(\"/\");\n if (segments.some((s) => s === \"..\")) {\n throw new Error('Path traversal (\"../\") is not allowed.');\n }\n if (filePath.startsWith(\"/\")) {\n if (!filePath.startsWith(\"/Volumes/\")) {\n throw new Error(\n 'Absolute paths must start with \"/Volumes/\". ' +\n \"Unity Catalog volume paths follow the format: /Volumes/<catalog>/<schema>/<volume>/\",\n );\n }\n return filePath;\n }\n if (!this.defaultVolume) {\n throw new Error(\n \"Cannot resolve relative path: no default volume set. Use an absolute path or set a default volume.\",\n );\n }\n return `${this.defaultVolume}/${filePath}`;\n }\n\n private async traced<T>(\n operation: string,\n attributes: Record<string, string>,\n fn: (span: Span) => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n // Pull any ambient attributes set by `runWithFilesSpanAttributes` (e.g.\n // `files.auth_mode` from the plugin layer). Static `attributes` win on\n // collision so callers can override per-call.\n const ambient = filesSpanAttributesStorage.getStore();\n\n return this.telemetry.startActiveSpan(\n `files.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"files.operation\": operation,\n ...(ambient ?? {}),\n ...attributes,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn(span);\n success = true;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n } finally {\n span.end();\n const duration = Date.now() - startTime;\n const metricAttrs = {\n \"files.operation\": operation,\n success: String(success),\n };\n this.telemetryMetrics.operationCount.add(1, metricAttrs);\n this.telemetryMetrics.operationDuration.record(duration, metricAttrs);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n async list(\n client: WorkspaceClient,\n directoryPath?: string,\n ): Promise<DirectoryEntry[]> {\n const resolvedPath = directoryPath\n ? this.resolvePath(directoryPath)\n : this.defaultVolume;\n if (!resolvedPath) {\n throw new Error(\"No directory path provided and no default volume set.\");\n }\n\n return this.traced(\"list\", { \"files.path\": resolvedPath }, async () => {\n const entries: DirectoryEntry[] = [];\n for await (const entry of client.files.listDirectoryContents({\n directory_path: resolvedPath,\n })) {\n entries.push(entry);\n }\n return entries;\n });\n }\n\n async read(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxSize?: number },\n ): Promise<string> {\n const resolvedPath = this.resolvePath(filePath);\n const maxSize = options?.maxSize ?? FILES_MAX_READ_SIZE;\n return this.traced(\"read\", { \"files.path\": resolvedPath }, async () => {\n const response = await this.download(client, filePath);\n if (!response.contents) {\n return \"\";\n }\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let result = \"\";\n let bytesRead = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > maxSize) {\n await reader.cancel();\n throw new Error(\n `File exceeds maximum read size (${maxSize} bytes). Use download() for large files.`,\n );\n }\n result += decoder.decode(value, { stream: true });\n }\n result += decoder.decode();\n return result;\n });\n }\n\n async download(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<DownloadResponse> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"download\", { \"files.path\": resolvedPath }, async () => {\n return client.files.download({\n file_path: resolvedPath,\n });\n });\n }\n\n async exists(client: WorkspaceClient, filePath: string): Promise<boolean> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"exists\", { \"files.path\": resolvedPath }, async () => {\n try {\n await this.metadata(client, filePath);\n return true;\n } catch (error) {\n if (error instanceof ApiError && error.statusCode === 404) {\n return false;\n }\n throw error;\n }\n });\n }\n\n async metadata(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<FileMetadata> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"metadata\", { \"files.path\": resolvedPath }, async () => {\n const response = await client.files.getMetadata({\n file_path: resolvedPath,\n });\n return {\n contentLength: response[\"content-length\"],\n contentType: contentTypeFromPath(\n filePath,\n response[\"content-type\"],\n this.customContentTypes,\n ),\n lastModified: response[\"last-modified\"],\n };\n });\n }\n\n async upload(\n client: WorkspaceClient,\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n\n return this.traced(\"upload\", { \"files.path\": resolvedPath }, async () => {\n const body = contents;\n const overwrite = options?.overwrite ?? true;\n\n // Workaround: The SDK's files.upload() has two bugs:\n // 1. It ignores the `contents` field (sets body to undefined)\n // 2. apiClient.request() checks `instanceof` against its own ReadableStream\n // subclass, so standard ReadableStream instances get JSON.stringified to \"{}\"\n // Bypass both by calling the REST API directly with SDK-provided auth.\n const hostValue = client.config.host;\n if (!hostValue) {\n throw new Error(\n \"Databricks host is not configured. Set DATABRICKS_HOST or configure client.config.host.\",\n );\n }\n const host = hostValue.startsWith(\"http\")\n ? hostValue\n : `https://${hostValue}`;\n const url = new URL(`/api/2.0/fs/files${resolvedPath}`, host);\n url.searchParams.set(\"overwrite\", String(overwrite));\n\n const headers = new Headers({\n \"Content-Type\": \"application/octet-stream\",\n \"User-Agent\": client.apiClient.userAgent(),\n });\n const fetchOptions: RequestInit = { method: \"PUT\", headers, body };\n\n if (body instanceof ReadableStream) {\n fetchOptions.duplex = \"half\";\n } else if (body instanceof Buffer) {\n headers.set(\"Content-Length\", String(body.length));\n } else if (typeof body === \"string\") {\n headers.set(\"Content-Length\", String(Buffer.byteLength(body)));\n }\n\n await client.config.authenticate(headers);\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text();\n logger.error(`Upload failed (${res.status}): ${text}`);\n const safeMessage = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n throw new ApiError(\n `Upload failed: ${safeMessage}`,\n \"UPLOAD_FAILED\",\n res.status,\n undefined,\n [],\n );\n }\n });\n }\n\n async createDirectory(\n client: WorkspaceClient,\n directoryPath: string,\n ): Promise<void> {\n const resolvedPath = this.resolvePath(directoryPath);\n return this.traced(\n \"createDirectory\",\n { \"files.path\": resolvedPath },\n async () => {\n await client.files.createDirectory({\n directory_path: resolvedPath,\n });\n },\n );\n }\n\n async delete(client: WorkspaceClient, filePath: string): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"delete\", { \"files.path\": resolvedPath }, async () => {\n await client.files.delete({\n file_path: resolvedPath,\n });\n });\n }\n\n async preview(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxChars?: number },\n ): Promise<FilePreview> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"preview\", { \"files.path\": resolvedPath }, async () => {\n const meta = await this.metadata(client, filePath);\n const isText = isTextContentType(meta.contentType);\n const isImage = meta.contentType?.startsWith(\"image/\") || false;\n\n if (!isText) {\n return { ...meta, textPreview: null, isText: false, isImage };\n }\n\n const response = await client.files.download({\n file_path: resolvedPath,\n });\n if (!response.contents) {\n return { ...meta, textPreview: \"\", isText: true, isImage: false };\n }\n\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let preview = \"\";\n const maxChars = options?.maxChars ?? 1024;\n\n while (preview.length < maxChars) {\n const { done, value } = await reader.read();\n if (done) break;\n preview += decoder.decode(value, { stream: true });\n }\n preview += decoder.decode();\n await reader.cancel();\n\n if (preview.length > maxChars) {\n preview = preview.slice(0, maxChars);\n }\n\n return { ...meta, textPreview: preview, isText: true, isImage: false };\n });\n }\n}\n"],"mappings":";;;;;;;;AAyBA,MAAM,SAAS,aAAa,mBAAmB;;;;;;;;;;;;;;AAe/C,MAAM,6BAA6B,IAAI,mBAEpC;;;;;;;;AASH,SAAgB,2BACd,YACA,IACY;AACZ,QAAO,2BAA2B,IAAI,YAAY,GAAG;;AAUvD,IAAa,iBAAb,MAA4B;CAC1B,AAAiB,OAAO;CACxB,AAAQ;CACR,AAAiB;CAEjB,AAAiB;CACjB,AAAiB;CAKjB,YAAY,QAA8B;AACxC,OAAK,gBAAgB,OAAO;AAC5B,OAAK,qBAAqB,OAAO;AAEjC,OAAK,YAAY,iBAAiB,YAAY,KAAK,MAAM,OAAO,UAAU;AAC1E,OAAK,mBAAmB;GACtB,gBAAgB,KAAK,UAClB,UAAU,CACV,cAAc,yBAAyB;IACtC,aAAa;IACb,MAAM;IACP,CAAC;GACJ,mBAAmB,KAAK,UACrB,UAAU,CACV,gBAAgB,4BAA4B;IAC3C,aAAa;IACb,MAAM;IACP,CAAC;GACL;;CAGH,YAAY,UAA0B;AACpC,MAAI,SAAS,SAAS,KACpB,OAAM,IAAI,MACR,uDAAuD,SAAS,OAAO,IACxE;AAEH,MAAI,SAAS,SAAS,KAAK,CACzB,OAAM,IAAI,MAAM,oCAAoC;AAItD,MADiB,SAAS,MAAM,IAAI,CACvB,MAAM,MAAM,MAAM,KAAK,CAClC,OAAM,IAAI,MAAM,2CAAyC;AAE3D,MAAI,SAAS,WAAW,IAAI,EAAE;AAC5B,OAAI,CAAC,SAAS,WAAW,YAAY,CACnC,OAAM,IAAI,MACR,oIAED;AAEH,UAAO;;AAET,MAAI,CAAC,KAAK,cACR,OAAM,IAAI,MACR,qGACD;AAEH,SAAO,GAAG,KAAK,cAAc,GAAG;;CAGlC,MAAc,OACZ,WACA,YACA,IACY;EACZ,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;EAKd,MAAM,UAAU,2BAA2B,UAAU;AAErD,SAAO,KAAK,UAAU,gBACpB,SAAS,aACT;GACE,MAAM,SAAS;GACf,YAAY;IACV,mBAAmB;IACnB,GAAI,WAAW,EAAE;IACjB,GAAG;IACJ;GACF,EACD,OAAO,SAAe;AACpB,OAAI;IACF,MAAM,SAAS,MAAM,GAAG,KAAK;AAC7B,cAAU;AACV,SAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAC3C,WAAO;YACA,OAAO;AACd,SAAK,gBAAgB,MAAe;AACpC,SAAK,UAAU;KACb,MAAM,eAAe;KACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;KAChE,CAAC;AACF,UAAM;aACE;AACR,SAAK,KAAK;IACV,MAAM,WAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,cAAc;KAClB,mBAAmB;KACnB,SAAS,OAAO,QAAQ;KACzB;AACD,SAAK,iBAAiB,eAAe,IAAI,GAAG,YAAY;AACxD,SAAK,iBAAiB,kBAAkB,OAAO,UAAU,YAAY;;KAGzE;GAAE,MAAM,KAAK;GAAM,eAAe;GAAM,CACzC;;CAGH,MAAM,KACJ,QACA,eAC2B;EAC3B,MAAM,eAAe,gBACjB,KAAK,YAAY,cAAc,GAC/B,KAAK;AACT,MAAI,CAAC,aACH,OAAM,IAAI,MAAM,wDAAwD;AAG1E,SAAO,KAAK,OAAO,QAAQ,EAAE,cAAc,cAAc,EAAE,YAAY;GACrE,MAAM,UAA4B,EAAE;AACpC,cAAW,MAAM,SAAS,OAAO,MAAM,sBAAsB,EAC3D,gBAAgB,cACjB,CAAC,CACA,SAAQ,KAAK,MAAM;AAErB,UAAO;IACP;;CAGJ,MAAM,KACJ,QACA,UACA,SACiB;EACjB,MAAM,eAAe,KAAK,YAAY,SAAS;EAC/C,MAAM,UAAU,SAAS,WAAW;AACpC,SAAO,KAAK,OAAO,QAAQ,EAAE,cAAc,cAAc,EAAE,YAAY;GACrE,MAAM,WAAW,MAAM,KAAK,SAAS,QAAQ,SAAS;AACtD,OAAI,CAAC,SAAS,SACZ,QAAO;GAET,MAAM,SAAS,SAAS,SAAS,WAAW;GAC5C,MAAM,UAAU,IAAI,aAAa;GACjC,IAAI,SAAS;GACb,IAAI,YAAY;AAChB,UAAO,MAAM;IACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,KAAM;AACV,iBAAa,MAAM;AACnB,QAAI,YAAY,SAAS;AACvB,WAAM,OAAO,QAAQ;AACrB,WAAM,IAAI,MACR,mCAAmC,QAAQ,0CAC5C;;AAEH,cAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;;AAEnD,aAAU,QAAQ,QAAQ;AAC1B,UAAO;IACP;;CAGJ,MAAM,SACJ,QACA,UAC2B;EAC3B,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,YAAY,EAAE,cAAc,cAAc,EAAE,YAAY;AACzE,UAAO,OAAO,MAAM,SAAS,EAC3B,WAAW,cACZ,CAAC;IACF;;CAGJ,MAAM,OAAO,QAAyB,UAAoC;EACxE,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,UAAU,EAAE,cAAc,cAAc,EAAE,YAAY;AACvE,OAAI;AACF,UAAM,KAAK,SAAS,QAAQ,SAAS;AACrC,WAAO;YACA,OAAO;AACd,QAAI,iBAAiB,YAAY,MAAM,eAAe,IACpD,QAAO;AAET,UAAM;;IAER;;CAGJ,MAAM,SACJ,QACA,UACuB;EACvB,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,YAAY,EAAE,cAAc,cAAc,EAAE,YAAY;GACzE,MAAM,WAAW,MAAM,OAAO,MAAM,YAAY,EAC9C,WAAW,cACZ,CAAC;AACF,UAAO;IACL,eAAe,SAAS;IACxB,aAAa,oBACX,UACA,SAAS,iBACT,KAAK,mBACN;IACD,cAAc,SAAS;IACxB;IACD;;CAGJ,MAAM,OACJ,QACA,UACA,UACA,SACe;EACf,MAAM,eAAe,KAAK,YAAY,SAAS;AAE/C,SAAO,KAAK,OAAO,UAAU,EAAE,cAAc,cAAc,EAAE,YAAY;GACvE,MAAM,OAAO;GACb,MAAM,YAAY,SAAS,aAAa;GAOxC,MAAM,YAAY,OAAO,OAAO;AAChC,OAAI,CAAC,UACH,OAAM,IAAI,MACR,0FACD;GAEH,MAAM,OAAO,UAAU,WAAW,OAAO,GACrC,YACA,WAAW;GACf,MAAM,MAAM,IAAI,IAAI,oBAAoB,gBAAgB,KAAK;AAC7D,OAAI,aAAa,IAAI,aAAa,OAAO,UAAU,CAAC;GAEpD,MAAM,UAAU,IAAI,QAAQ;IAC1B,gBAAgB;IAChB,cAAc,OAAO,UAAU,WAAW;IAC3C,CAAC;GACF,MAAM,eAA4B;IAAE,QAAQ;IAAO;IAAS;IAAM;AAElE,OAAI,gBAAgB,eAClB,cAAa,SAAS;YACb,gBAAgB,OACzB,SAAQ,IAAI,kBAAkB,OAAO,KAAK,OAAO,CAAC;YACzC,OAAO,SAAS,SACzB,SAAQ,IAAI,kBAAkB,OAAO,OAAO,WAAW,KAAK,CAAC,CAAC;AAGhE,SAAM,OAAO,OAAO,aAAa,QAAQ;GAEzC,MAAM,MAAM,MAAM,MAAM,IAAI,UAAU,EAAE,aAAa;AAErD,OAAI,CAAC,IAAI,IAAI;IACX,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,WAAO,MAAM,kBAAkB,IAAI,OAAO,KAAK,OAAO;AAEtD,UAAM,IAAI,SACR,kBAFkB,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,QAGjE,iBACA,IAAI,QACJ,QACA,EAAE,CACH;;IAEH;;CAGJ,MAAM,gBACJ,QACA,eACe;EACf,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,SAAO,KAAK,OACV,mBACA,EAAE,cAAc,cAAc,EAC9B,YAAY;AACV,SAAM,OAAO,MAAM,gBAAgB,EACjC,gBAAgB,cACjB,CAAC;IAEL;;CAGH,MAAM,OAAO,QAAyB,UAAiC;EACrE,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,UAAU,EAAE,cAAc,cAAc,EAAE,YAAY;AACvE,SAAM,OAAO,MAAM,OAAO,EACxB,WAAW,cACZ,CAAC;IACF;;CAGJ,MAAM,QACJ,QACA,UACA,SACsB;EACtB,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,WAAW,EAAE,cAAc,cAAc,EAAE,YAAY;GACxE,MAAM,OAAO,MAAM,KAAK,SAAS,QAAQ,SAAS;GAClD,MAAM,SAAS,kBAAkB,KAAK,YAAY;GAClD,MAAM,UAAU,KAAK,aAAa,WAAW,SAAS,IAAI;AAE1D,OAAI,CAAC,OACH,QAAO;IAAE,GAAG;IAAM,aAAa;IAAM,QAAQ;IAAO;IAAS;GAG/D,MAAM,WAAW,MAAM,OAAO,MAAM,SAAS,EAC3C,WAAW,cACZ,CAAC;AACF,OAAI,CAAC,SAAS,SACZ,QAAO;IAAE,GAAG;IAAM,aAAa;IAAI,QAAQ;IAAM,SAAS;IAAO;GAGnE,MAAM,SAAS,SAAS,SAAS,WAAW;GAC5C,MAAM,UAAU,IAAI,aAAa;GACjC,IAAI,UAAU;GACd,MAAM,WAAW,SAAS,YAAY;AAEtC,UAAO,QAAQ,SAAS,UAAU;IAChC,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,KAAM;AACV,eAAW,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;;AAEpD,cAAW,QAAQ,QAAQ;AAC3B,SAAM,OAAO,QAAQ;AAErB,OAAI,QAAQ,SAAS,SACnB,WAAU,QAAQ,MAAM,GAAG,SAAS;AAGtC,UAAO;IAAE,GAAG;IAAM,aAAa;IAAS,QAAQ;IAAM,SAAS;IAAO;IACtE"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/files/client.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type { TelemetryOptions } from \"shared\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport type {\n DirectoryEntry,\n DownloadResponse,\n FileMetadata,\n FilePreview,\n} from \"../../plugins/files/types\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport { ApiError, type WorkspaceClient } from \"../../workspace-client\";\nimport {\n contentTypeFromPath,\n FILES_MAX_READ_SIZE,\n isTextContentType,\n} from \"./defaults\";\n\nconst logger = createLogger(\"connectors:files\");\n\n/**\n * Ambient span-attribute propagation for `FilesConnector.traced()`.\n *\n * Callers (e.g. the plugin's `_withAuthModeAttributes` wrapper) set extra\n * span attributes here via `runWithFilesSpanAttributes(attrs, fn)`. The\n * connector's `traced()` decorator reads them and merges them into the\n * span it creates around the SDK call. This lets the plugin tag spans with\n * `files.auth_mode` without opening a duplicate `files.<op>` span.\n *\n * AsyncLocalStorage is used so concurrent requests don't see each other's\n * attributes. Outside an active scope, `getStore()` returns `undefined` and\n * the connector falls back to the static attribute set.\n */\nconst filesSpanAttributesStorage = new AsyncLocalStorage<\n Record<string, string>\n>();\n\n/**\n * Run `fn` with the supplied attributes attached to whatever span the\n * `FilesConnector` opens for its SDK call. Used to propagate request-scoped\n * attributes (e.g. `files.auth_mode`) onto the connector's span without\n * opening a parent span — avoids the 2x span allocation that\n * `startActiveSpan` parented otherwise.\n */\nexport function runWithFilesSpanAttributes<T>(\n attributes: Record<string, string>,\n fn: () => Promise<T>,\n): Promise<T> {\n return filesSpanAttributesStorage.run(attributes, fn);\n}\n\ninterface FilesConnectorConfig {\n defaultVolume?: string;\n timeout?: number;\n telemetry?: TelemetryOptions;\n customContentTypes?: Record<string, string>;\n}\n\nexport class FilesConnector {\n private readonly name = \"files\";\n private defaultVolume: string | undefined;\n private readonly customContentTypes: Record<string, string> | undefined;\n\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n operationCount: Counter;\n operationDuration: Histogram;\n };\n\n constructor(config: FilesConnectorConfig) {\n this.defaultVolume = config.defaultVolume;\n this.customContentTypes = config.customContentTypes;\n\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.telemetryMetrics = {\n operationCount: this.telemetry\n .getMeter()\n .createCounter(\"files.operation.count\", {\n description: \"Total number of file operations\",\n unit: \"1\",\n }),\n operationDuration: this.telemetry\n .getMeter()\n .createHistogram(\"files.operation.duration\", {\n description: \"Duration of file operations\",\n unit: \"ms\",\n }),\n };\n }\n\n resolvePath(filePath: string): string {\n if (filePath.length > 4096) {\n throw new Error(\n `Path exceeds maximum length of 4096 characters (got ${filePath.length}).`,\n );\n }\n if (filePath.includes(\"\\0\")) {\n throw new Error(\"Path must not contain null bytes.\");\n }\n\n const segments = filePath.split(\"/\");\n if (segments.some((s) => s === \"..\")) {\n throw new Error('Path traversal (\"../\") is not allowed.');\n }\n if (filePath.startsWith(\"/\")) {\n if (!filePath.startsWith(\"/Volumes/\")) {\n throw new Error(\n 'Absolute paths must start with \"/Volumes/\". ' +\n \"Unity Catalog volume paths follow the format: /Volumes/<catalog>/<schema>/<volume>/\",\n );\n }\n return filePath;\n }\n if (!this.defaultVolume) {\n throw new Error(\n \"Cannot resolve relative path: no default volume set. Use an absolute path or set a default volume.\",\n );\n }\n return `${this.defaultVolume}/${filePath}`;\n }\n\n private async traced<T>(\n operation: string,\n attributes: Record<string, string>,\n fn: (span: Span) => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n // Pull any ambient attributes set by `runWithFilesSpanAttributes` (e.g.\n // `files.auth_mode` from the plugin layer). Static `attributes` win on\n // collision so callers can override per-call.\n const ambient = filesSpanAttributesStorage.getStore();\n\n return this.telemetry.startActiveSpan(\n `files.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"files.operation\": operation,\n ...(ambient ?? {}),\n ...attributes,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn(span);\n success = true;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n throw error;\n } finally {\n span.end();\n const duration = Date.now() - startTime;\n const metricAttrs = {\n \"files.operation\": operation,\n success: String(success),\n };\n this.telemetryMetrics.operationCount.add(1, metricAttrs);\n this.telemetryMetrics.operationDuration.record(duration, metricAttrs);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n async list(\n client: WorkspaceClient,\n directoryPath?: string,\n ): Promise<DirectoryEntry[]> {\n const resolvedPath = directoryPath\n ? this.resolvePath(directoryPath)\n : this.defaultVolume;\n if (!resolvedPath) {\n throw new Error(\"No directory path provided and no default volume set.\");\n }\n\n return this.traced(\"list\", { \"files.path\": resolvedPath }, async () => {\n const entries: DirectoryEntry[] = [];\n for await (const entry of client.files.listDirectoryContents({\n directory_path: resolvedPath,\n })) {\n entries.push(entry);\n }\n return entries;\n });\n }\n\n async read(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxSize?: number },\n ): Promise<string> {\n const resolvedPath = this.resolvePath(filePath);\n const maxSize = options?.maxSize ?? FILES_MAX_READ_SIZE;\n return this.traced(\"read\", { \"files.path\": resolvedPath }, async () => {\n const response = await this.download(client, filePath);\n if (!response.contents) {\n return \"\";\n }\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let result = \"\";\n let bytesRead = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > maxSize) {\n await reader.cancel();\n throw new Error(\n `File exceeds maximum read size (${maxSize} bytes). Use download() for large files.`,\n );\n }\n result += decoder.decode(value, { stream: true });\n }\n result += decoder.decode();\n return result;\n });\n }\n\n async download(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<DownloadResponse> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"download\", { \"files.path\": resolvedPath }, async () => {\n return client.files.download({\n file_path: resolvedPath,\n });\n });\n }\n\n async exists(client: WorkspaceClient, filePath: string): Promise<boolean> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"exists\", { \"files.path\": resolvedPath }, async () => {\n try {\n await this.metadata(client, filePath);\n return true;\n } catch (error) {\n if (error instanceof ApiError && error.statusCode === 404) {\n return false;\n }\n throw error;\n }\n });\n }\n\n async metadata(\n client: WorkspaceClient,\n filePath: string,\n ): Promise<FileMetadata> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"metadata\", { \"files.path\": resolvedPath }, async () => {\n const response = await client.files.getMetadata({\n file_path: resolvedPath,\n });\n return {\n contentLength: response[\"content-length\"],\n contentType: contentTypeFromPath(\n filePath,\n response[\"content-type\"],\n this.customContentTypes,\n ),\n lastModified: response[\"last-modified\"],\n };\n });\n }\n\n async upload(\n client: WorkspaceClient,\n filePath: string,\n contents: ReadableStream | Buffer | string,\n options?: { overwrite?: boolean },\n ): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n\n return this.traced(\"upload\", { \"files.path\": resolvedPath }, async () => {\n const body = contents;\n const overwrite = options?.overwrite ?? true;\n\n // Workaround: The SDK's files.upload() has two bugs:\n // 1. It ignores the `contents` field (sets body to undefined)\n // 2. apiClient.request() checks `instanceof` against its own ReadableStream\n // subclass, so standard ReadableStream instances get JSON.stringified to \"{}\"\n // Bypass both by calling the REST API directly with SDK-provided auth.\n const hostValue = client.config.host;\n if (!hostValue) {\n throw new Error(\n \"Databricks host is not configured. Set DATABRICKS_HOST or configure client.config.host.\",\n );\n }\n const host = hostValue.startsWith(\"http\")\n ? hostValue\n : `https://${hostValue}`;\n const url = new URL(`/api/2.0/fs/files${resolvedPath}`, host);\n url.searchParams.set(\"overwrite\", String(overwrite));\n\n const headers = new Headers({\n \"Content-Type\": \"application/octet-stream\",\n \"User-Agent\": client.apiClient.userAgent(),\n });\n const fetchOptions: RequestInit = { method: \"PUT\", headers, body };\n\n if (body instanceof ReadableStream) {\n fetchOptions.duplex = \"half\";\n } else if (body instanceof Buffer) {\n headers.set(\"Content-Length\", String(body.length));\n } else if (typeof body === \"string\") {\n headers.set(\"Content-Length\", String(Buffer.byteLength(body)));\n }\n\n await client.config.authenticate(headers);\n\n const res = await fetch(url.toString(), fetchOptions);\n\n if (!res.ok) {\n const text = await res.text();\n logger.error(`Upload failed (${res.status}): ${text}`);\n const safeMessage = text.length > 200 ? `${text.slice(0, 200)}…` : text;\n throw new ApiError(\n `Upload failed: ${safeMessage}`,\n \"UPLOAD_FAILED\",\n res.status,\n undefined,\n [],\n );\n }\n });\n }\n\n async createDirectory(\n client: WorkspaceClient,\n directoryPath: string,\n ): Promise<void> {\n const resolvedPath = this.resolvePath(directoryPath);\n return this.traced(\n \"createDirectory\",\n { \"files.path\": resolvedPath },\n async () => {\n await client.files.createDirectory({\n directory_path: resolvedPath,\n });\n },\n );\n }\n\n async delete(client: WorkspaceClient, filePath: string): Promise<void> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"delete\", { \"files.path\": resolvedPath }, async () => {\n await client.files.delete({\n file_path: resolvedPath,\n });\n });\n }\n\n async preview(\n client: WorkspaceClient,\n filePath: string,\n options?: { maxChars?: number },\n ): Promise<FilePreview> {\n const resolvedPath = this.resolvePath(filePath);\n return this.traced(\"preview\", { \"files.path\": resolvedPath }, async () => {\n const meta = await this.metadata(client, filePath);\n const isText = isTextContentType(meta.contentType);\n const isImage = meta.contentType?.startsWith(\"image/\") || false;\n\n if (!isText) {\n return { ...meta, textPreview: null, isText: false, isImage };\n }\n\n const response = await client.files.download({\n file_path: resolvedPath,\n });\n if (!response.contents) {\n return { ...meta, textPreview: \"\", isText: true, isImage: false };\n }\n\n const reader = response.contents.getReader();\n const decoder = new TextDecoder();\n let preview = \"\";\n const maxChars = options?.maxChars ?? 1024;\n\n while (preview.length < maxChars) {\n const { done, value } = await reader.read();\n if (done) break;\n preview += decoder.decode(value, { stream: true });\n }\n preview += decoder.decode();\n await reader.cancel();\n\n if (preview.length > maxChars) {\n preview = preview.slice(0, maxChars);\n }\n\n return { ...meta, textPreview: preview, isText: true, isImage: false };\n });\n }\n}\n"],"mappings":";;;;;;;;AA2BA,MAAM,SAAS,aAAa,mBAAmB;;;;;;;;;;;;;;AAe/C,MAAM,6BAA6B,IAAI,mBAEpC;;;;;;;;AASH,SAAgB,2BACd,YACA,IACY;AACZ,QAAO,2BAA2B,IAAI,YAAY,GAAG;;AAUvD,IAAa,iBAAb,MAA4B;CAC1B,AAAiB,OAAO;CACxB,AAAQ;CACR,AAAiB;CAEjB,AAAiB;CACjB,AAAiB;CAKjB,YAAY,QAA8B;AACxC,OAAK,gBAAgB,OAAO;AAC5B,OAAK,qBAAqB,OAAO;AAEjC,OAAK,YAAY,iBAAiB,YAAY,KAAK,MAAM,OAAO,UAAU;AAC1E,OAAK,mBAAmB;GACtB,gBAAgB,KAAK,UAClB,UAAU,CACV,cAAc,yBAAyB;IACtC,aAAa;IACb,MAAM;IACP,CAAC;GACJ,mBAAmB,KAAK,UACrB,UAAU,CACV,gBAAgB,4BAA4B;IAC3C,aAAa;IACb,MAAM;IACP,CAAC;GACL;;CAGH,YAAY,UAA0B;AACpC,MAAI,SAAS,SAAS,KACpB,OAAM,IAAI,MACR,uDAAuD,SAAS,OAAO,IACxE;AAEH,MAAI,SAAS,SAAS,KAAK,CACzB,OAAM,IAAI,MAAM,oCAAoC;AAItD,MADiB,SAAS,MAAM,IAAI,CACvB,MAAM,MAAM,MAAM,KAAK,CAClC,OAAM,IAAI,MAAM,2CAAyC;AAE3D,MAAI,SAAS,WAAW,IAAI,EAAE;AAC5B,OAAI,CAAC,SAAS,WAAW,YAAY,CACnC,OAAM,IAAI,MACR,oIAED;AAEH,UAAO;;AAET,MAAI,CAAC,KAAK,cACR,OAAM,IAAI,MACR,qGACD;AAEH,SAAO,GAAG,KAAK,cAAc,GAAG;;CAGlC,MAAc,OACZ,WACA,YACA,IACY;EACZ,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;EAKd,MAAM,UAAU,2BAA2B,UAAU;AAErD,SAAO,KAAK,UAAU,gBACpB,SAAS,aACT;GACE,MAAM,SAAS;GACf,YAAY;IACV,mBAAmB;IACnB,GAAI,WAAW,EAAE;IACjB,GAAG;IACJ;GACF,EACD,OAAO,SAAe;AACpB,OAAI;IACF,MAAM,SAAS,MAAM,GAAG,KAAK;AAC7B,cAAU;AACV,SAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAC3C,WAAO;YACA,OAAO;AACd,SAAK,gBAAgB,MAAe;AACpC,SAAK,UAAU;KACb,MAAM,eAAe;KACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;KAChE,CAAC;AACF,UAAM;aACE;AACR,SAAK,KAAK;IACV,MAAM,WAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,cAAc;KAClB,mBAAmB;KACnB,SAAS,OAAO,QAAQ;KACzB;AACD,SAAK,iBAAiB,eAAe,IAAI,GAAG,YAAY;AACxD,SAAK,iBAAiB,kBAAkB,OAAO,UAAU,YAAY;;KAGzE;GAAE,MAAM,KAAK;GAAM,eAAe;GAAM,CACzC;;CAGH,MAAM,KACJ,QACA,eAC2B;EAC3B,MAAM,eAAe,gBACjB,KAAK,YAAY,cAAc,GAC/B,KAAK;AACT,MAAI,CAAC,aACH,OAAM,IAAI,MAAM,wDAAwD;AAG1E,SAAO,KAAK,OAAO,QAAQ,EAAE,cAAc,cAAc,EAAE,YAAY;GACrE,MAAM,UAA4B,EAAE;AACpC,cAAW,MAAM,SAAS,OAAO,MAAM,sBAAsB,EAC3D,gBAAgB,cACjB,CAAC,CACA,SAAQ,KAAK,MAAM;AAErB,UAAO;IACP;;CAGJ,MAAM,KACJ,QACA,UACA,SACiB;EACjB,MAAM,eAAe,KAAK,YAAY,SAAS;EAC/C,MAAM,UAAU,SAAS,WAAW;AACpC,SAAO,KAAK,OAAO,QAAQ,EAAE,cAAc,cAAc,EAAE,YAAY;GACrE,MAAM,WAAW,MAAM,KAAK,SAAS,QAAQ,SAAS;AACtD,OAAI,CAAC,SAAS,SACZ,QAAO;GAET,MAAM,SAAS,SAAS,SAAS,WAAW;GAC5C,MAAM,UAAU,IAAI,aAAa;GACjC,IAAI,SAAS;GACb,IAAI,YAAY;AAChB,UAAO,MAAM;IACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,KAAM;AACV,iBAAa,MAAM;AACnB,QAAI,YAAY,SAAS;AACvB,WAAM,OAAO,QAAQ;AACrB,WAAM,IAAI,MACR,mCAAmC,QAAQ,0CAC5C;;AAEH,cAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;;AAEnD,aAAU,QAAQ,QAAQ;AAC1B,UAAO;IACP;;CAGJ,MAAM,SACJ,QACA,UAC2B;EAC3B,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,YAAY,EAAE,cAAc,cAAc,EAAE,YAAY;AACzE,UAAO,OAAO,MAAM,SAAS,EAC3B,WAAW,cACZ,CAAC;IACF;;CAGJ,MAAM,OAAO,QAAyB,UAAoC;EACxE,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,UAAU,EAAE,cAAc,cAAc,EAAE,YAAY;AACvE,OAAI;AACF,UAAM,KAAK,SAAS,QAAQ,SAAS;AACrC,WAAO;YACA,OAAO;AACd,QAAI,iBAAiB,YAAY,MAAM,eAAe,IACpD,QAAO;AAET,UAAM;;IAER;;CAGJ,MAAM,SACJ,QACA,UACuB;EACvB,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,YAAY,EAAE,cAAc,cAAc,EAAE,YAAY;GACzE,MAAM,WAAW,MAAM,OAAO,MAAM,YAAY,EAC9C,WAAW,cACZ,CAAC;AACF,UAAO;IACL,eAAe,SAAS;IACxB,aAAa,oBACX,UACA,SAAS,iBACT,KAAK,mBACN;IACD,cAAc,SAAS;IACxB;IACD;;CAGJ,MAAM,OACJ,QACA,UACA,UACA,SACe;EACf,MAAM,eAAe,KAAK,YAAY,SAAS;AAE/C,SAAO,KAAK,OAAO,UAAU,EAAE,cAAc,cAAc,EAAE,YAAY;GACvE,MAAM,OAAO;GACb,MAAM,YAAY,SAAS,aAAa;GAOxC,MAAM,YAAY,OAAO,OAAO;AAChC,OAAI,CAAC,UACH,OAAM,IAAI,MACR,0FACD;GAEH,MAAM,OAAO,UAAU,WAAW,OAAO,GACrC,YACA,WAAW;GACf,MAAM,MAAM,IAAI,IAAI,oBAAoB,gBAAgB,KAAK;AAC7D,OAAI,aAAa,IAAI,aAAa,OAAO,UAAU,CAAC;GAEpD,MAAM,UAAU,IAAI,QAAQ;IAC1B,gBAAgB;IAChB,cAAc,OAAO,UAAU,WAAW;IAC3C,CAAC;GACF,MAAM,eAA4B;IAAE,QAAQ;IAAO;IAAS;IAAM;AAElE,OAAI,gBAAgB,eAClB,cAAa,SAAS;YACb,gBAAgB,OACzB,SAAQ,IAAI,kBAAkB,OAAO,KAAK,OAAO,CAAC;YACzC,OAAO,SAAS,SACzB,SAAQ,IAAI,kBAAkB,OAAO,OAAO,WAAW,KAAK,CAAC,CAAC;AAGhE,SAAM,OAAO,OAAO,aAAa,QAAQ;GAEzC,MAAM,MAAM,MAAM,MAAM,IAAI,UAAU,EAAE,aAAa;AAErD,OAAI,CAAC,IAAI,IAAI;IACX,MAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,WAAO,MAAM,kBAAkB,IAAI,OAAO,KAAK,OAAO;AAEtD,UAAM,IAAI,SACR,kBAFkB,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,KAAK,QAGjE,iBACA,IAAI,QACJ,QACA,EAAE,CACH;;IAEH;;CAGJ,MAAM,gBACJ,QACA,eACe;EACf,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,SAAO,KAAK,OACV,mBACA,EAAE,cAAc,cAAc,EAC9B,YAAY;AACV,SAAM,OAAO,MAAM,gBAAgB,EACjC,gBAAgB,cACjB,CAAC;IAEL;;CAGH,MAAM,OAAO,QAAyB,UAAiC;EACrE,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,UAAU,EAAE,cAAc,cAAc,EAAE,YAAY;AACvE,SAAM,OAAO,MAAM,OAAO,EACxB,WAAW,cACZ,CAAC;IACF;;CAGJ,MAAM,QACJ,QACA,UACA,SACsB;EACtB,MAAM,eAAe,KAAK,YAAY,SAAS;AAC/C,SAAO,KAAK,OAAO,WAAW,EAAE,cAAc,cAAc,EAAE,YAAY;GACxE,MAAM,OAAO,MAAM,KAAK,SAAS,QAAQ,SAAS;GAClD,MAAM,SAAS,kBAAkB,KAAK,YAAY;GAClD,MAAM,UAAU,KAAK,aAAa,WAAW,SAAS,IAAI;AAE1D,OAAI,CAAC,OACH,QAAO;IAAE,GAAG;IAAM,aAAa;IAAM,QAAQ;IAAO;IAAS;GAG/D,MAAM,WAAW,MAAM,OAAO,MAAM,SAAS,EAC3C,WAAW,cACZ,CAAC;AACF,OAAI,CAAC,SAAS,SACZ,QAAO;IAAE,GAAG;IAAM,aAAa;IAAI,QAAQ;IAAM,SAAS;IAAO;GAGnE,MAAM,SAAS,SAAS,SAAS,WAAW;GAC5C,MAAM,UAAU,IAAI,aAAa;GACjC,IAAI,UAAU;GACd,MAAM,WAAW,SAAS,YAAY;AAEtC,UAAO,QAAQ,SAAS,UAAU;IAChC,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,QAAI,KAAM;AACV,eAAW,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;;AAEpD,cAAW,QAAQ,QAAQ;AAC3B,SAAM,OAAO,QAAQ;AAErB,OAAI,QAAQ,SAAS,SACnB,WAAU,QAAQ,MAAM,GAAG,SAAS;AAGtC,UAAO;IAAE,GAAG;IAAM,aAAa;IAAS,QAAQ;IAAM,SAAS;IAAO;IACtE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/connectors/lakebase/index.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/connectors/lakebase/index.ts"],"mappings":";;;;;;;;;;AAeA;;;iBAAgB,oBAAA,CAAmB,MAAA,GAAS,OAAA,CAAQ,kBAAA,IAAsB,IAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["createLakebasePool","createLakebasePoolBase"],"sources":["../../../src/connectors/lakebase/index.ts"],"sourcesContent":["import {\n createLakebasePool as createLakebasePoolBase,\n type LakebasePoolConfig,\n} from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\nimport { createLogger } from \"../../logging/logger\";\n\n/**\n * Create a Lakebase pool with appkit's logger integration.\n * Telemetry automatically uses appkit's OpenTelemetry configuration via global registry.\n *\n * @param config - Lakebase pool configuration\n * @returns PostgreSQL pool with appkit integration\n */\nexport function createLakebasePool(config?: Partial<LakebasePoolConfig>): Pool {\n return createLakebasePoolBase({\n logger: createLogger(\"connectors:lakebase\"),\n ...config,\n });\n}\n\n// Re-export everything else from lakebase\nexport {\n type DatabaseCredential,\n type GenerateDatabaseCredentialRequest,\n generateDatabaseCredential,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n getWorkspaceClient,\n type LakebasePoolConfig,\n type RequestedClaims,\n RequestedClaimsPermissionSet,\n type RequestedResource,\n} from \"@databricks/lakebase\";\n\nexport {\n createLakebasePoolManager,\n type LakebasePoolManager,\n} from \"./pool-manager\";\n\nexport { type LakebasePool, RoutingPool } from \"./routing-pool\";\n"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","names":["createLakebasePool","createLakebasePoolBase"],"sources":["../../../src/connectors/lakebase/index.ts"],"sourcesContent":["import {\n createLakebasePool as createLakebasePoolBase,\n type LakebasePoolConfig,\n} from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\n\nimport { createLogger } from \"../../logging/logger\";\n\n/**\n * Create a Lakebase pool with appkit's logger integration.\n * Telemetry automatically uses appkit's OpenTelemetry configuration via global registry.\n *\n * @param config - Lakebase pool configuration\n * @returns PostgreSQL pool with appkit integration\n */\nexport function createLakebasePool(config?: Partial<LakebasePoolConfig>): Pool {\n return createLakebasePoolBase({\n logger: createLogger(\"connectors:lakebase\"),\n ...config,\n });\n}\n\n// Re-export everything else from lakebase\nexport {\n type DatabaseCredential,\n type GenerateDatabaseCredentialRequest,\n generateDatabaseCredential,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n getWorkspaceClient,\n type LakebasePoolConfig,\n type RequestedClaims,\n RequestedClaimsPermissionSet,\n type RequestedResource,\n} from \"@databricks/lakebase\";\n\nexport {\n createLakebasePoolManager,\n type LakebasePoolManager,\n} from \"./pool-manager\";\n\nexport { type LakebasePool, RoutingPool } from \"./routing-pool\";\n"],"mappings":";;;;;;;;;;;;;AAeA,SAAgBA,qBAAmB,QAA4C;AAC7E,QAAOC,mBAAuB;EAC5B,QAAQ,aAAa,sBAAsB;EAC3C,GAAG;EACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-manager.d.ts","names":[],"sources":["../../../src/connectors/lakebase/pool-manager.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"pool-manager.d.ts","names":[],"sources":["../../../src/connectors/lakebase/pool-manager.ts"],"mappings":";;;;;;AAcA;;;;UAAiB,mBAAA;EAaZ;;;;;;;;EAJH,OAAA,CACE,GAAA,UACA,aAAA,EAAe,OAAA,CAAQ,kBAAA,GACvB,gBAAA,YACC,IAAA;EAFD;EAKF,OAAA,CAAQ,GAAA;EAHL;EAMH,SAAA,CAAU,GAAA,WAAc,OAAA;EAHhB;EAMR,QAAA,IAAY,OAAA;EAHF;EAAA,SAMD,IAAA;AAAA;;;;;AAwBX;;;;;;;;;;;;;;;;;iBAAgB,yBAAA,CACd,UAAA,GAAa,OAAA,CAAQ,kBAAA,IACpB,mBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-manager.js","names":[],"sources":["../../../src/connectors/lakebase/pool-manager.ts"],"sourcesContent":["import type { LakebasePoolConfig } from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\nimport { createLakebasePool } from \"./index\";\n\n/** Interval for removing empty (connectionless) pools from the Map. */\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).\n *\n * Used for On-Behalf-Of (OBO) scenarios where each user needs their own pool\n * with their own OAuth token refresh, enabling features like Row-Level Security.\n */\nexport interface LakebasePoolManager {\n /**\n * Get an existing pool or create a new one for the given key.\n * When creating, merges `perPoolConfig` with the base config passed to the factory.\n *\n * If `tokenFingerprint` is provided and differs from the cached pool's\n * fingerprint, the stale pool is closed and a fresh one is created with\n * the new config (including the updated `workspaceClient`).\n */\n getPool(\n key: string,\n perPoolConfig: Partial<LakebasePoolConfig>,\n tokenFingerprint?: string,\n ): Pool;\n\n /** Check whether a pool exists for the given key. */\n hasPool(key: string): boolean;\n\n /** Close and remove a specific pool. */\n closePool(key: string): Promise<void>;\n\n /** Close all managed pools and stop cleanup (for graceful shutdown). */\n closeAll(): Promise<void>;\n\n /** Number of active pools. */\n readonly size: number;\n}\n\n/**\n * Create a pool manager that maintains per-key Lakebase connection pools.\n *\n * Each pool is created via `createLakebasePool` with the base config merged\n * with per-pool overrides (e.g. a user's `workspaceClient` and `user`).\n *\n * A periodic cleanup removes empty Pool objects (where all connections have\n * been closed by pg's built-in `idleTimeoutMillis`) from the internal Map.\n *\n * @example OBO usage\n * ```typescript\n * const poolManager = createLakebasePoolManager();\n *\n * // In a route handler:\n * const userPool = poolManager.getPool(userName, {\n * workspaceClient: new WorkspaceClient({ token: userToken, host, authType: \"pat\" }),\n * user: userName,\n * });\n * const result = await userPool.query(\"SELECT * FROM products\");\n * ```\n */\nexport function createLakebasePoolManager(\n baseConfig?: Partial<LakebasePoolConfig>,\n): LakebasePoolManager {\n interface PoolEntry {\n pool: Pool;\n tokenFingerprint?: string;\n }\n\n const entries = new Map<string, PoolEntry>();\n\n // Periodically remove empty Pool objects from the Map.\n // pg.Pool's idleTimeoutMillis closes idle connections automatically;\n // this just cleans up the Map entries once all connections are gone.\n const cleanupTimer = setInterval(() => {\n for (const [key, entry] of entries) {\n if (entry.pool.totalCount === 0) {\n entry.pool.end().catch(() => {});\n entries.delete(key);\n }\n }\n }, CLEANUP_INTERVAL_MS);\n cleanupTimer.unref();\n\n return {\n getPool(\n key: string,\n perPoolConfig: Partial<LakebasePoolConfig>,\n tokenFingerprint?: string,\n ): Pool {\n const existing = entries.get(key);\n\n if (existing) {\n // When the caller provides a fingerprint that differs from the\n // cached one, the underlying OBO token has rotated. The pool's\n // password callback holds a stale WorkspaceClient (authType: \"pat\",\n // static token) that will fail once the Lakebase Postgres token\n // needs refreshing. Drain the old pool and create a fresh one.\n const stale =\n tokenFingerprint &&\n existing.tokenFingerprint &&\n tokenFingerprint !== existing.tokenFingerprint;\n\n if (!stale) return existing.pool;\n\n existing.pool.end().catch(() => {});\n }\n\n // Safe without locking: createLakebasePool is synchronous and Node.js\n // is single-threaded, so no preemption between get() and set().\n const pool = createLakebasePool({ ...baseConfig, ...perPoolConfig });\n entries.set(key, { pool, tokenFingerprint });\n return pool;\n },\n\n hasPool(key: string): boolean {\n return entries.has(key);\n },\n\n async closePool(key: string): Promise<void> {\n const entry = entries.get(key);\n if (entry) {\n await entry.pool.end();\n entries.delete(key);\n }\n },\n\n async closeAll(): Promise<void> {\n clearInterval(cleanupTimer);\n const endPromises = [...entries.values()].map((e) => e.pool.end());\n await Promise.all(endPromises);\n entries.clear();\n },\n\n get size() {\n return entries.size;\n },\n };\n}\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"pool-manager.js","names":[],"sources":["../../../src/connectors/lakebase/pool-manager.ts"],"sourcesContent":["import type { LakebasePoolConfig } from \"@databricks/lakebase\";\nimport type { Pool } from \"pg\";\n\nimport { createLakebasePool } from \"./index\";\n\n/** Interval for removing empty (connectionless) pools from the Map. */\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes\n\n/**\n * Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).\n *\n * Used for On-Behalf-Of (OBO) scenarios where each user needs their own pool\n * with their own OAuth token refresh, enabling features like Row-Level Security.\n */\nexport interface LakebasePoolManager {\n /**\n * Get an existing pool or create a new one for the given key.\n * When creating, merges `perPoolConfig` with the base config passed to the factory.\n *\n * If `tokenFingerprint` is provided and differs from the cached pool's\n * fingerprint, the stale pool is closed and a fresh one is created with\n * the new config (including the updated `workspaceClient`).\n */\n getPool(\n key: string,\n perPoolConfig: Partial<LakebasePoolConfig>,\n tokenFingerprint?: string,\n ): Pool;\n\n /** Check whether a pool exists for the given key. */\n hasPool(key: string): boolean;\n\n /** Close and remove a specific pool. */\n closePool(key: string): Promise<void>;\n\n /** Close all managed pools and stop cleanup (for graceful shutdown). */\n closeAll(): Promise<void>;\n\n /** Number of active pools. */\n readonly size: number;\n}\n\n/**\n * Create a pool manager that maintains per-key Lakebase connection pools.\n *\n * Each pool is created via `createLakebasePool` with the base config merged\n * with per-pool overrides (e.g. a user's `workspaceClient` and `user`).\n *\n * A periodic cleanup removes empty Pool objects (where all connections have\n * been closed by pg's built-in `idleTimeoutMillis`) from the internal Map.\n *\n * @example OBO usage\n * ```typescript\n * const poolManager = createLakebasePoolManager();\n *\n * // In a route handler:\n * const userPool = poolManager.getPool(userName, {\n * workspaceClient: new WorkspaceClient({ token: userToken, host, authType: \"pat\" }),\n * user: userName,\n * });\n * const result = await userPool.query(\"SELECT * FROM products\");\n * ```\n */\nexport function createLakebasePoolManager(\n baseConfig?: Partial<LakebasePoolConfig>,\n): LakebasePoolManager {\n interface PoolEntry {\n pool: Pool;\n tokenFingerprint?: string;\n }\n\n const entries = new Map<string, PoolEntry>();\n\n // Periodically remove empty Pool objects from the Map.\n // pg.Pool's idleTimeoutMillis closes idle connections automatically;\n // this just cleans up the Map entries once all connections are gone.\n const cleanupTimer = setInterval(() => {\n for (const [key, entry] of entries) {\n if (entry.pool.totalCount === 0) {\n entry.pool.end().catch(() => {});\n entries.delete(key);\n }\n }\n }, CLEANUP_INTERVAL_MS);\n cleanupTimer.unref();\n\n return {\n getPool(\n key: string,\n perPoolConfig: Partial<LakebasePoolConfig>,\n tokenFingerprint?: string,\n ): Pool {\n const existing = entries.get(key);\n\n if (existing) {\n // When the caller provides a fingerprint that differs from the\n // cached one, the underlying OBO token has rotated. The pool's\n // password callback holds a stale WorkspaceClient (authType: \"pat\",\n // static token) that will fail once the Lakebase Postgres token\n // needs refreshing. Drain the old pool and create a fresh one.\n const stale =\n tokenFingerprint &&\n existing.tokenFingerprint &&\n tokenFingerprint !== existing.tokenFingerprint;\n\n if (!stale) return existing.pool;\n\n existing.pool.end().catch(() => {});\n }\n\n // Safe without locking: createLakebasePool is synchronous and Node.js\n // is single-threaded, so no preemption between get() and set().\n const pool = createLakebasePool({ ...baseConfig, ...perPoolConfig });\n entries.set(key, { pool, tokenFingerprint });\n return pool;\n },\n\n hasPool(key: string): boolean {\n return entries.has(key);\n },\n\n async closePool(key: string): Promise<void> {\n const entry = entries.get(key);\n if (entry) {\n await entry.pool.end();\n entries.delete(key);\n }\n },\n\n async closeAll(): Promise<void> {\n clearInterval(cleanupTimer);\n const endPromises = [...entries.values()].map((e) => e.pool.end());\n await Promise.all(endPromises);\n entries.clear();\n },\n\n get size() {\n return entries.size;\n },\n };\n}\n"],"mappings":";;;;AAMA,MAAM,sBAAsB,MAAS;;;;;;;;;;;;;;;;;;;;;;AAyDrC,SAAgB,0BACd,YACqB;CAMrB,MAAM,0BAAU,IAAI,KAAwB;CAK5C,MAAM,eAAe,kBAAkB;AACrC,OAAK,MAAM,CAAC,KAAK,UAAU,QACzB,KAAI,MAAM,KAAK,eAAe,GAAG;AAC/B,SAAM,KAAK,KAAK,CAAC,YAAY,GAAG;AAChC,WAAQ,OAAO,IAAI;;IAGtB,oBAAoB;AACvB,cAAa,OAAO;AAEpB,QAAO;EACL,QACE,KACA,eACA,kBACM;GACN,MAAM,WAAW,QAAQ,IAAI,IAAI;AAEjC,OAAI,UAAU;AAWZ,QAAI,EAJF,oBACA,SAAS,oBACT,qBAAqB,SAAS,kBAEpB,QAAO,SAAS;AAE5B,aAAS,KAAK,KAAK,CAAC,YAAY,GAAG;;GAKrC,MAAM,OAAO,mBAAmB;IAAE,GAAG;IAAY,GAAG;IAAe,CAAC;AACpE,WAAQ,IAAI,KAAK;IAAE;IAAM;IAAkB,CAAC;AAC5C,UAAO;;EAGT,QAAQ,KAAsB;AAC5B,UAAO,QAAQ,IAAI,IAAI;;EAGzB,MAAM,UAAU,KAA4B;GAC1C,MAAM,QAAQ,QAAQ,IAAI,IAAI;AAC9B,OAAI,OAAO;AACT,UAAM,MAAM,KAAK,KAAK;AACtB,YAAQ,OAAO,IAAI;;;EAIvB,MAAM,WAA0B;AAC9B,iBAAc,aAAa;GAC3B,MAAM,cAAc,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,KAAK,CAAC;AAClE,SAAM,QAAQ,IAAI,YAAY;AAC9B,WAAQ,OAAO;;EAGjB,IAAI,OAAO;AACT,UAAO,QAAQ;;EAElB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routing-pool.d.ts","names":[],"sources":["../../../src/connectors/lakebase/routing-pool.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"routing-pool.d.ts","names":[],"sources":["../../../src/connectors/lakebase/routing-pool.ts"],"mappings":";;;;;;AAYA;;;;;UAAiB,YAAA;EACf,KAAA,WAAgB,cAAA,QACd,IAAA,UACA,MAAA,eACC,OAAA,CAAQ,WAAA,CAAY,CAAA;EACvB,OAAA,IAAW,OAAA,CAAQ,UAAA;EACnB,GAAA,IAAO,OAAA;EAAA,SACE,UAAA;EAAA,SACA,SAAA;EAAA,SACA,YAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routing-pool.js","names":[],"sources":["../../../src/connectors/lakebase/routing-pool.ts"],"sourcesContent":["import type { Pool, PoolClient, QueryResult, QueryResultRow } from \"pg\";\nimport { getUserContext } from \"../../context/execution-context\";\nimport type { UserContext } from \"../../context/user-context\";\n\n/**\n * Subset of `pg.Pool` exposed by the Lakebase plugin.\n *\n * RoutingPool does not extend EventEmitter — event listener methods\n * like `on('error', ...)` are not available. Use `query()`, `connect()`,\n * and `end()` for all pool operations.\n */\nexport interface LakebasePool {\n query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>>;\n connect(): Promise<PoolClient>;\n end(): Promise<void>;\n readonly totalCount: number;\n readonly idleCount: number;\n readonly waitingCount: number;\n}\n\n/**\n * A `pg.Pool`-like wrapper that routes queries to the appropriate pool\n * based on the current execution context.\n *\n * When called inside `runInUserContext()` (set up by `Plugin.asUser(req)`),\n * queries route to the per-user pool returned by `resolveUserPool`.\n * Otherwise, queries route to the service-principal pool.\n *\n * This enables OBO (On-Behalf-Of) without custom `asUser()` overrides —\n * the base class sets up AsyncLocalStorage context, and the RoutingPool\n * reads it transparently.\n */\nexport class RoutingPool implements LakebasePool {\n constructor(\n private spPool: Pool,\n private resolveUserPool: (ctx: UserContext) => Pool,\n ) {}\n\n private activePool(): Pool {\n const userCtx = getUserContext();\n return userCtx ? this.resolveUserPool(userCtx) : this.spPool;\n }\n\n query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n return this.activePool().query<T>(text, values);\n }\n\n connect(): Promise<PoolClient> {\n return this.activePool().connect();\n }\n\n async end(): Promise<void> {\n await this.spPool.end();\n }\n\n get totalCount() {\n return this.spPool.totalCount;\n }\n get idleCount() {\n return this.spPool.idleCount;\n }\n get waitingCount() {\n return this.spPool.waitingCount;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"routing-pool.js","names":[],"sources":["../../../src/connectors/lakebase/routing-pool.ts"],"sourcesContent":["import type { Pool, PoolClient, QueryResult, QueryResultRow } from \"pg\";\n\nimport { getUserContext } from \"../../context/execution-context\";\nimport type { UserContext } from \"../../context/user-context\";\n\n/**\n * Subset of `pg.Pool` exposed by the Lakebase plugin.\n *\n * RoutingPool does not extend EventEmitter — event listener methods\n * like `on('error', ...)` are not available. Use `query()`, `connect()`,\n * and `end()` for all pool operations.\n */\nexport interface LakebasePool {\n query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>>;\n connect(): Promise<PoolClient>;\n end(): Promise<void>;\n readonly totalCount: number;\n readonly idleCount: number;\n readonly waitingCount: number;\n}\n\n/**\n * A `pg.Pool`-like wrapper that routes queries to the appropriate pool\n * based on the current execution context.\n *\n * When called inside `runInUserContext()` (set up by `Plugin.asUser(req)`),\n * queries route to the per-user pool returned by `resolveUserPool`.\n * Otherwise, queries route to the service-principal pool.\n *\n * This enables OBO (On-Behalf-Of) without custom `asUser()` overrides —\n * the base class sets up AsyncLocalStorage context, and the RoutingPool\n * reads it transparently.\n */\nexport class RoutingPool implements LakebasePool {\n constructor(\n private spPool: Pool,\n private resolveUserPool: (ctx: UserContext) => Pool,\n ) {}\n\n private activePool(): Pool {\n const userCtx = getUserContext();\n return userCtx ? this.resolveUserPool(userCtx) : this.spPool;\n }\n\n query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n return this.activePool().query<T>(text, values);\n }\n\n connect(): Promise<PoolClient> {\n return this.activePool().connect();\n }\n\n async end(): Promise<void> {\n await this.spPool.end();\n }\n\n get totalCount() {\n return this.spPool.totalCount;\n }\n get idleCount() {\n return this.spPool.idleCount;\n }\n get waitingCount() {\n return this.spPool.waitingCount;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoCA,IAAa,cAAb,MAAiD;CAC/C,YACE,AAAQ,QACR,AAAQ,iBACR;EAFQ;EACA;;CAGV,AAAQ,aAAmB;EACzB,MAAM,UAAU,gBAAgB;AAChC,SAAO,UAAU,KAAK,gBAAgB,QAAQ,GAAG,KAAK;;CAGxD,MACE,MACA,QACyB;AACzB,SAAO,KAAK,YAAY,CAAC,MAAS,MAAM,OAAO;;CAGjD,UAA+B;AAC7B,SAAO,KAAK,YAAY,CAAC,SAAS;;CAGpC,MAAM,MAAqB;AACzB,QAAM,KAAK,OAAO,KAAK;;CAGzB,IAAI,aAAa;AACf,SAAO,KAAK,OAAO;;CAErB,IAAI,YAAY;AACd,SAAO,KAAK,OAAO;;CAErB,IAAI,eAAe;AACjB,SAAO,KAAK,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","names":[],"sources":["../../../src/connectors/mcp/client.ts"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"client.d.ts","names":[],"sources":["../../../src/connectors/mcp/client.ts"],"mappings":";;;;;;;;;;;UAwJiB,mBAAA;EACf,SAAA;EACA,MAAA,EAAQ,KAAA;IAAQ,IAAA;IAAc,KAAA,EAAO,KAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;cA+B1B,eAAA;EAAA,QAOD,aAAA;EAAA,QACA,YAAA;EAAA,QACA,MAAA;EAAA,QACA,OAAA;EAAA,QATF,WAAA;EAAA,QACA,UAAA;EAAA,QACA,SAAA;EAAA,QACA,MAAA;cAGE,aAAA,UACA,YAAA,QAAoB,OAAA,CAAQ,MAAA,mBAC5B,MAAA,EAAQ,aAAA,EACR,OAAA;IAAW,SAAA,GAAY,SAAA;IAAW,SAAA,UAAmB,KAAA;EAAA;EAqH/D;;;;;;;;;;;;;EArGM,UAAA,CACJ,SAAA,EAAW,iBAAA,KACV,OAAA,CAAQ,mBAAA;EAAA,QAoBH,UAAA;EAUF,OAAA,CAAQ,QAAA,EAAU,iBAAA,GAAoB,OAAA;EAqE5C,qBAAA,CAAA,GAAyB,mBAAA;EA6OO;;;;;;EAzNhC,uBAAA,CAAwB,UAAA;EAIlB,QAAA,CACJ,aAAA,UACA,IAAA,WACA,WAAA,GAAc,MAAA,kBACd,YAAA,GAAe,WAAA,GACd,OAAA;EAoDG,KAAA,CAAA,GAAS,OAAA;EAAA,QAMD,OAAA;EAAA,QA2FA,gBAAA;;;;;;UA2DA,kBAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/mcp/client.ts"],"sourcesContent":["/**\n * Custom MCP over HTTP (Streamable) — not `@modelcontextprotocol/sdk`\n *\n * This module implements a tiny JSON-RPC 2.0 client on `fetch` for the subset\n * of MCP we need: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call` over a single JSON request/response. We do not use the official\n * SDK because:\n *\n * - **Policy and auth are the product** — every outbound URL is checked with\n * {@link McpHostPolicy} (allowlist, DNS, private/blocked IP ranges) before\n * the first byte is sent, and workspace tokens are only forwarded when\n * `forwardWorkspaceAuth` is true for that destination. A generic transport\n * from the SDK would still need the same hooks; re-wrapping it would be\n * about as much code, with a larger third-party surface to audit.\n * - **Narrow scope** — we only target Databricks-hosted MCP over Streamable\n * HTTP, not stdio, full SSE sessions, or the rest of the protocol. A\n * hand-rolled path keeps the call graph obvious in code review.\n * - **Zero extra runtime dependency** for this path, consistent with other\n * small, security-sensitive AppKit pieces.\n *\n * Revisit if we add more transports, or if the SDK ships a first-class way to\n * inject our host policy and per-URL auth without fighting the default\n * transport.\n */\nimport type { AgentToolDefinition } from \"shared\";\nimport { APPKIT_USER_AGENT } from \"../../context/client-options\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n assertResolvedHostSafe,\n checkMcpUrl,\n type DnsLookup,\n type McpHostPolicy,\n} from \"./host-policy\";\nimport type { McpEndpointConfig } from \"./types\";\n\nconst logger = createLogger(\"connector:mcp\");\n\n/**\n * Hard cap on the size of a single MCP response body, including SSE\n * frames bundled into one HTTP response. MCP `initialize` / `tools/list`\n * / `tools/call` responses are JSON-RPC payloads — single-digit kilobytes\n * in normal use. A response anywhere near this size signals either a\n * misbehaving server or an attempt to exhaust client memory; we'd rather\n * fail loudly than allocate unbounded buffers from a remote.\n */\nconst MCP_RESPONSE_BODY_LIMIT_BYTES = 1024 * 1024;\n\n/**\n * Read a fetch Response body into a string with a hard size cap. Aborts\n * and throws if the cumulative bytes read cross {@link\n * MCP_RESPONSE_BODY_LIMIT_BYTES}, so a remote server cannot keep\n * streaming data past the limit. Returns the empty string when the\n * response has no readable body.\n */\n/**\n * Empty-object fallback used when an MCP server ships a missing or\n * malformed `inputSchema`. Matches the shape downstream adapters expect\n * for a function tool that takes no arguments.\n */\nconst EMPTY_TOOL_PARAMETERS: AgentToolDefinition[\"parameters\"] = {\n type: \"object\",\n properties: {},\n};\n\n/**\n * Coerce a remote MCP server's reported `inputSchema` into the\n * JSONSchema7 shape AppKit's adapters expect for a function tool's\n * `parameters`. The MCP wire type is `Record<string, unknown>`, so a\n * misbehaving (or malicious) server could ship arbitrary JSON. We accept\n * only the standard `{ type: \"object\", properties: {...} }` shape and\n * fall back to an empty-parameters schema otherwise — the tool still\n * registers, it just can't accept arguments.\n */\nfunction coerceToolParameters(\n inputSchema: Record<string, unknown> | undefined,\n): AgentToolDefinition[\"parameters\"] {\n if (!inputSchema || typeof inputSchema !== \"object\") {\n return EMPTY_TOOL_PARAMETERS;\n }\n const { type, properties } = inputSchema;\n if (type !== \"object\") return EMPTY_TOOL_PARAMETERS;\n if (\n properties !== undefined &&\n (typeof properties !== \"object\" ||\n properties === null ||\n Array.isArray(properties))\n ) {\n return EMPTY_TOOL_PARAMETERS;\n }\n return inputSchema as AgentToolDefinition[\"parameters\"];\n}\n\nasync function readResponseTextCapped(\n response: Response,\n maxBytes: number,\n contextLabel: string,\n): Promise<string> {\n if (!response.body) return \"\";\n const reader = response.body.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let total = 0;\n let out = \"\";\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n throw new Error(\n `MCP ${contextLabel}: response body exceeded ${maxBytes} bytes — refusing to allocate unbounded buffer from a remote server.`,\n );\n }\n out += decoder.decode(value, { stream: true });\n }\n out += decoder.decode();\n } finally {\n reader.releaseLock();\n }\n return out;\n}\n\ninterface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id: number;\n method: string;\n params?: Record<string, unknown>;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id: number;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\ninterface McpToolSchema {\n name: string;\n description?: string;\n inputSchema?: Record<string, unknown>;\n}\n\ninterface McpToolCallResult {\n content: Array<{ type: string; text?: string }>;\n isError?: boolean;\n}\n\n/**\n * Per-endpoint outcome of {@link AppKitMcpClient.connectAll}. Callers (the\n * agents plugin in particular) use the split to warn at startup when some\n * MCP servers are unreachable without aborting boot for the rest.\n */\nexport interface McpConnectAllResult {\n connected: string[];\n failed: Array<{ name: string; error: Error }>;\n}\n\ninterface McpServerConnection {\n config: McpEndpointConfig;\n resolvedUrl: string;\n /**\n * Whether workspace auth (SP / OBO) may be forwarded to this endpoint's URL.\n * Decided at `connect()` time via {@link McpHostPolicy} and cached for the\n * lifetime of the connection.\n */\n forwardWorkspaceAuth: boolean;\n tools: Map<string, McpToolSchema>;\n}\n\n/**\n * Lightweight MCP client for Databricks-hosted MCP servers.\n *\n * Uses raw fetch() with JSON-RPC 2.0 over HTTP — no @modelcontextprotocol/sdk\n * or LangChain dependency. Supports the Streamable HTTP transport only\n * (POST with JSON-RPC request, single JSON-RPC response). Implements exactly\n * four methods: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call`. No prompts/resources/completion/sampling.\n *\n * All outbound URLs are gated by an {@link McpHostPolicy}: unallowlisted hosts\n * are rejected before the first byte is sent, and workspace credentials are\n * only forwarded to the same-origin workspace. See `mcp-host-policy.ts`.\n *\n * Rationale for hand-rolling JSON-RPC instead of `@modelcontextprotocol/sdk`:\n * see the file-level comment at the top of this module.\n */\nexport class AppKitMcpClient {\n private connections = new Map<string, McpServerConnection>();\n private sessionIds = new Map<string, string>();\n private requestId = 0;\n private closed = false;\n\n constructor(\n private workspaceHost: string,\n private authenticate: () => Promise<Record<string, string>>,\n private policy: McpHostPolicy,\n private options: { dnsLookup?: DnsLookup; fetchImpl?: typeof fetch } = {},\n ) {}\n\n /**\n * Connects every endpoint in parallel and returns a structured summary so\n * callers can distinguish \"all connected\" from \"some failed\".\n *\n * Returning the result instead of throwing is deliberate: one\n * misconfigured MCP server should not take down the entire agents plugin\n * at boot, and the agents plugin uses the summary to warn at startup with\n * the failed-endpoint names. Errors are also logged here so a caller\n * that ignores the return still gets per-endpoint diagnostics.\n *\n * @returns `connected` lists the endpoint names that initialised\n * successfully; `failed` carries `{ name, error }` for the rest.\n */\n async connectAll(\n endpoints: McpEndpointConfig[],\n ): Promise<McpConnectAllResult> {\n const results = await Promise.allSettled(\n endpoints.map((ep) => this.connect(ep)),\n );\n const out: McpConnectAllResult = { connected: [], failed: [] };\n for (let i = 0; i < results.length; i++) {\n const r = results[i];\n const name = endpoints[i].name;\n if (r.status === \"fulfilled\") {\n out.connected.push(name);\n } else {\n const error =\n r.reason instanceof Error ? r.reason : new Error(String(r.reason));\n logger.error(\"Failed to connect MCP server %s: %O\", name, error);\n out.failed.push({ name, error });\n }\n }\n return out;\n }\n\n private resolveUrl(endpoint: McpEndpointConfig): string {\n if (\n endpoint.url.startsWith(\"http://\") ||\n endpoint.url.startsWith(\"https://\")\n ) {\n return endpoint.url;\n }\n return `${this.workspaceHost}${endpoint.url}`;\n }\n\n async connect(endpoint: McpEndpointConfig): Promise<void> {\n const resolvedUrl = this.resolveUrl(endpoint);\n const check = checkMcpUrl(resolvedUrl, this.policy);\n if (!check.ok) {\n throw new Error(\n `MCP endpoint '${endpoint.name}' refused at connect: ${check.reason}`,\n );\n }\n await assertResolvedHostSafe(\n check.url.hostname,\n this.policy,\n this.options.dnsLookup,\n );\n\n logger.info(\n \"Connecting to MCP server: %s at %s (forwardWorkspaceAuth=%s)\",\n endpoint.name,\n resolvedUrl,\n check.forwardWorkspaceAuth,\n );\n\n const initResponse = await this.sendRpc(\n resolvedUrl,\n \"initialize\",\n {\n protocolVersion: \"2025-03-26\",\n capabilities: {},\n clientInfo: { name: \"appkit-agent\", version: \"0.1.0\" },\n },\n { forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n\n if (initResponse.sessionId) {\n this.sessionIds.set(endpoint.name, initResponse.sessionId);\n }\n const sessionId = this.sessionIds.get(endpoint.name);\n\n await this.sendNotification(resolvedUrl, \"notifications/initialized\", {\n sessionId,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n });\n\n const listResponse = await this.sendRpc(\n resolvedUrl,\n \"tools/list\",\n {},\n { sessionId, forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n const toolList =\n (listResponse.result as { tools?: McpToolSchema[] })?.tools ?? [];\n\n const tools = new Map<string, McpToolSchema>();\n for (const tool of toolList) {\n tools.set(tool.name, tool);\n }\n\n this.connections.set(endpoint.name, {\n config: endpoint,\n resolvedUrl,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n tools,\n });\n logger.info(\n \"Connected to MCP server %s: %d tools available\",\n endpoint.name,\n tools.size,\n );\n }\n\n getAllToolDefinitions(): AgentToolDefinition[] {\n const defs: AgentToolDefinition[] = [];\n for (const [serverName, conn] of this.connections) {\n for (const [toolName, schema] of conn.tools) {\n defs.push({\n name: `mcp.${serverName}.${toolName}`,\n description: schema.description ?? toolName,\n parameters: coerceToolParameters(schema.inputSchema),\n });\n }\n }\n return defs;\n }\n\n /**\n * Whether the named MCP server may receive workspace-scoped auth headers\n * (e.g., an OBO bearer token from an end-user request). Callers should gate\n * auth-forwarding decisions on this to prevent credential exfiltration to\n * non-workspace hosts.\n */\n canForwardWorkspaceAuth(serverName: string): boolean {\n return this.connections.get(serverName)?.forwardWorkspaceAuth ?? false;\n }\n\n async callTool(\n qualifiedName: string,\n args: unknown,\n authHeaders?: Record<string, string>,\n callerSignal?: AbortSignal,\n ): Promise<string> {\n const parts = qualifiedName.split(\".\");\n if (parts.length < 3 || parts[0] !== \"mcp\") {\n throw new Error(`Invalid MCP tool name: ${qualifiedName}`);\n }\n const serverName = parts[1];\n const toolName = parts.slice(2).join(\".\");\n\n const conn = this.connections.get(serverName);\n if (!conn) {\n throw new Error(`MCP server not connected: ${serverName}`);\n }\n\n const sessionId = this.sessionIds.get(serverName);\n // authHeaders are caller-supplied credentials (typically the OBO token).\n // Only honor them if the destination URL was admitted with\n // forwardWorkspaceAuth=true at connect time.\n const scopedAuthOverride = conn.forwardWorkspaceAuth\n ? authHeaders\n : undefined;\n\n const rpcResult = await this.sendRpc(\n conn.resolvedUrl,\n \"tools/call\",\n { name: toolName, arguments: args },\n {\n authOverride: scopedAuthOverride,\n sessionId,\n forwardWorkspaceAuth: conn.forwardWorkspaceAuth,\n callerSignal,\n },\n );\n const result = rpcResult.result as McpToolCallResult;\n\n // `text` is optional on `McpToolCallResult.content[]` per the MCP\n // spec; filtering only on `type === \"text\"` lets `c.text` be\n // `undefined`, which `Array.join` would render as the literal\n // string `\"undefined\"` and ship to the agent. Narrow on both\n // fields so the joined string only contains real text.\n const textContent = (result.content ?? []).filter(\n (c): c is { type: \"text\"; text: string } =>\n c.type === \"text\" && typeof c.text === \"string\",\n );\n\n if (result.isError) {\n const errText = textContent.map((c) => c.text).join(\"\\n\");\n throw new Error(errText || \"MCP tool call failed\");\n }\n\n return textContent.map((c) => c.text).join(\"\\n\");\n }\n\n async close(): Promise<void> {\n this.closed = true;\n this.connections.clear();\n this.sessionIds.clear();\n }\n\n private async sendRpc(\n url: string,\n method: string,\n params?: Record<string, unknown>,\n options?: {\n authOverride?: Record<string, string>;\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n /**\n * Optional external abort signal (typically the agent's stream signal).\n * Composed with the built-in 30 s timeout so `/cancel` or agent-run\n * shutdown immediately propagates to the MCP fetch rather than waiting\n * for the remote server to respond.\n */\n callerSignal?: AbortSignal;\n },\n ): Promise<{ result: unknown; sessionId?: string }> {\n if (this.closed) throw new Error(\"MCP client is closed\");\n\n const request: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id: ++this.requestId,\n method,\n ...(params && { params }),\n };\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"User-Agent\": APPKIT_USER_AGENT,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n const signals: AbortSignal[] = [AbortSignal.timeout(30_000)];\n if (options?.callerSignal) signals.push(options.callerSignal);\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(request),\n signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],\n });\n\n if (!response.ok) {\n throw new Error(\n `MCP request to ${method} failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n // Always read the body via the capped helper so a misconfigured or\n // malicious server can't exhaust client memory by streaming an\n // unbounded payload. Applies to both SSE (`response.text()` would\n // have buffered the whole stream) and plain JSON (`response.json()`\n // does the same internally).\n const bodyText = await readResponseTextCapped(\n response,\n MCP_RESPONSE_BODY_LIMIT_BYTES,\n method,\n );\n let json: JsonRpcResponse;\n\n if (contentType.includes(\"text/event-stream\")) {\n const lastData = bodyText\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data: \"))\n .map((line) => line.slice(6))\n .pop();\n if (!lastData) {\n throw new Error(`MCP SSE response for ${method} contained no data`);\n }\n json = JSON.parse(lastData) as JsonRpcResponse;\n } else {\n if (bodyText.length === 0) {\n throw new Error(`MCP response for ${method} had an empty body`);\n }\n json = JSON.parse(bodyText) as JsonRpcResponse;\n }\n\n if (json.error) {\n throw new Error(`MCP error (${json.error.code}): ${json.error.message}`);\n }\n\n const sid = response.headers.get(\"mcp-session-id\") ?? undefined;\n return { result: json.result, sessionId: sid };\n }\n\n private async sendNotification(\n url: string,\n method: string,\n options?: {\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n },\n ): Promise<void> {\n if (this.closed) return;\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"User-Agent\": APPKIT_USER_AGENT,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n // MCP notifications are fire-and-forget per spec — we don't throw on\n // failure. But silently swallowing 4xx/5xx hides server-side\n // rejections that would otherwise look like a successful connect()\n // followed by mysterious tool-call failures. Surface the bad status\n // via the logger so the dev sees it without breaking the protocol\n // contract.\n try {\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ jsonrpc: \"2.0\", method }),\n signal: AbortSignal.timeout(30_000),\n });\n if (!response.ok) {\n logger.warn(\n \"MCP notification %s to %s returned %d %s — the server may have rejected the request, but per MCP spec notifications are fire-and-forget and the connection is considered established.\",\n method,\n url,\n response.status,\n response.statusText,\n );\n }\n } catch (err) {\n logger.warn(\n \"MCP notification %s to %s failed before a response was received: %O\",\n method,\n url,\n err,\n );\n }\n }\n\n /**\n * Return the auth headers to send on an outbound request. Workspace auth\n * (SP or OBO) is only resolved when `forwardWorkspaceAuth` is true; for\n * non-workspace hosts no bearer token is attached.\n */\n private async resolveAuthHeaders(options?: {\n authOverride?: Record<string, string>;\n forwardWorkspaceAuth?: boolean;\n }): Promise<Record<string, string>> {\n if (!options?.forwardWorkspaceAuth) return {};\n if (options.authOverride) return options.authOverride;\n return this.authenticate();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/mcp/client.ts"],"sourcesContent":["/**\n * Custom MCP over HTTP (Streamable) — not `@modelcontextprotocol/sdk`\n *\n * This module implements a tiny JSON-RPC 2.0 client on `fetch` for the subset\n * of MCP we need: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call` over a single JSON request/response. We do not use the official\n * SDK because:\n *\n * - **Policy and auth are the product** — every outbound URL is checked with\n * {@link McpHostPolicy} (allowlist, DNS, private/blocked IP ranges) before\n * the first byte is sent, and workspace tokens are only forwarded when\n * `forwardWorkspaceAuth` is true for that destination. A generic transport\n * from the SDK would still need the same hooks; re-wrapping it would be\n * about as much code, with a larger third-party surface to audit.\n * - **Narrow scope** — we only target Databricks-hosted MCP over Streamable\n * HTTP, not stdio, full SSE sessions, or the rest of the protocol. A\n * hand-rolled path keeps the call graph obvious in code review.\n * - **Zero extra runtime dependency** for this path, consistent with other\n * small, security-sensitive AppKit pieces.\n *\n * Revisit if we add more transports, or if the SDK ships a first-class way to\n * inject our host policy and per-URL auth without fighting the default\n * transport.\n */\nimport type { AgentToolDefinition } from \"shared\";\n\nimport { APPKIT_USER_AGENT } from \"../../context/client-options\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n assertResolvedHostSafe,\n checkMcpUrl,\n type DnsLookup,\n type McpHostPolicy,\n} from \"./host-policy\";\nimport type { McpEndpointConfig } from \"./types\";\n\nconst logger = createLogger(\"connector:mcp\");\n\n/**\n * Hard cap on the size of a single MCP response body, including SSE\n * frames bundled into one HTTP response. MCP `initialize` / `tools/list`\n * / `tools/call` responses are JSON-RPC payloads — single-digit kilobytes\n * in normal use. A response anywhere near this size signals either a\n * misbehaving server or an attempt to exhaust client memory; we'd rather\n * fail loudly than allocate unbounded buffers from a remote.\n */\nconst MCP_RESPONSE_BODY_LIMIT_BYTES = 1024 * 1024;\n\n/**\n * Read a fetch Response body into a string with a hard size cap. Aborts\n * and throws if the cumulative bytes read cross {@link\n * MCP_RESPONSE_BODY_LIMIT_BYTES}, so a remote server cannot keep\n * streaming data past the limit. Returns the empty string when the\n * response has no readable body.\n */\n/**\n * Empty-object fallback used when an MCP server ships a missing or\n * malformed `inputSchema`. Matches the shape downstream adapters expect\n * for a function tool that takes no arguments.\n */\nconst EMPTY_TOOL_PARAMETERS: AgentToolDefinition[\"parameters\"] = {\n type: \"object\",\n properties: {},\n};\n\n/**\n * Coerce a remote MCP server's reported `inputSchema` into the\n * JSONSchema7 shape AppKit's adapters expect for a function tool's\n * `parameters`. The MCP wire type is `Record<string, unknown>`, so a\n * misbehaving (or malicious) server could ship arbitrary JSON. We accept\n * only the standard `{ type: \"object\", properties: {...} }` shape and\n * fall back to an empty-parameters schema otherwise — the tool still\n * registers, it just can't accept arguments.\n */\nfunction coerceToolParameters(\n inputSchema: Record<string, unknown> | undefined,\n): AgentToolDefinition[\"parameters\"] {\n if (!inputSchema || typeof inputSchema !== \"object\") {\n return EMPTY_TOOL_PARAMETERS;\n }\n const { type, properties } = inputSchema;\n if (type !== \"object\") return EMPTY_TOOL_PARAMETERS;\n if (\n properties !== undefined &&\n (typeof properties !== \"object\" ||\n properties === null ||\n Array.isArray(properties))\n ) {\n return EMPTY_TOOL_PARAMETERS;\n }\n return inputSchema as AgentToolDefinition[\"parameters\"];\n}\n\nasync function readResponseTextCapped(\n response: Response,\n maxBytes: number,\n contextLabel: string,\n): Promise<string> {\n if (!response.body) return \"\";\n const reader = response.body.getReader();\n const decoder = new TextDecoder(\"utf-8\");\n let total = 0;\n let out = \"\";\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n throw new Error(\n `MCP ${contextLabel}: response body exceeded ${maxBytes} bytes — refusing to allocate unbounded buffer from a remote server.`,\n );\n }\n out += decoder.decode(value, { stream: true });\n }\n out += decoder.decode();\n } finally {\n reader.releaseLock();\n }\n return out;\n}\n\ninterface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id: number;\n method: string;\n params?: Record<string, unknown>;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id: number;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\ninterface McpToolSchema {\n name: string;\n description?: string;\n inputSchema?: Record<string, unknown>;\n}\n\ninterface McpToolCallResult {\n content: Array<{ type: string; text?: string }>;\n isError?: boolean;\n}\n\n/**\n * Per-endpoint outcome of {@link AppKitMcpClient.connectAll}. Callers (the\n * agents plugin in particular) use the split to warn at startup when some\n * MCP servers are unreachable without aborting boot for the rest.\n */\nexport interface McpConnectAllResult {\n connected: string[];\n failed: Array<{ name: string; error: Error }>;\n}\n\ninterface McpServerConnection {\n config: McpEndpointConfig;\n resolvedUrl: string;\n /**\n * Whether workspace auth (SP / OBO) may be forwarded to this endpoint's URL.\n * Decided at `connect()` time via {@link McpHostPolicy} and cached for the\n * lifetime of the connection.\n */\n forwardWorkspaceAuth: boolean;\n tools: Map<string, McpToolSchema>;\n}\n\n/**\n * Lightweight MCP client for Databricks-hosted MCP servers.\n *\n * Uses raw fetch() with JSON-RPC 2.0 over HTTP — no @modelcontextprotocol/sdk\n * or LangChain dependency. Supports the Streamable HTTP transport only\n * (POST with JSON-RPC request, single JSON-RPC response). Implements exactly\n * four methods: `initialize`, `notifications/initialized`, `tools/list`,\n * `tools/call`. No prompts/resources/completion/sampling.\n *\n * All outbound URLs are gated by an {@link McpHostPolicy}: unallowlisted hosts\n * are rejected before the first byte is sent, and workspace credentials are\n * only forwarded to the same-origin workspace. See `mcp-host-policy.ts`.\n *\n * Rationale for hand-rolling JSON-RPC instead of `@modelcontextprotocol/sdk`:\n * see the file-level comment at the top of this module.\n */\nexport class AppKitMcpClient {\n private connections = new Map<string, McpServerConnection>();\n private sessionIds = new Map<string, string>();\n private requestId = 0;\n private closed = false;\n\n constructor(\n private workspaceHost: string,\n private authenticate: () => Promise<Record<string, string>>,\n private policy: McpHostPolicy,\n private options: { dnsLookup?: DnsLookup; fetchImpl?: typeof fetch } = {},\n ) {}\n\n /**\n * Connects every endpoint in parallel and returns a structured summary so\n * callers can distinguish \"all connected\" from \"some failed\".\n *\n * Returning the result instead of throwing is deliberate: one\n * misconfigured MCP server should not take down the entire agents plugin\n * at boot, and the agents plugin uses the summary to warn at startup with\n * the failed-endpoint names. Errors are also logged here so a caller\n * that ignores the return still gets per-endpoint diagnostics.\n *\n * @returns `connected` lists the endpoint names that initialised\n * successfully; `failed` carries `{ name, error }` for the rest.\n */\n async connectAll(\n endpoints: McpEndpointConfig[],\n ): Promise<McpConnectAllResult> {\n const results = await Promise.allSettled(\n endpoints.map((ep) => this.connect(ep)),\n );\n const out: McpConnectAllResult = { connected: [], failed: [] };\n for (let i = 0; i < results.length; i++) {\n const r = results[i];\n const name = endpoints[i].name;\n if (r.status === \"fulfilled\") {\n out.connected.push(name);\n } else {\n const error =\n r.reason instanceof Error ? r.reason : new Error(String(r.reason));\n logger.error(\"Failed to connect MCP server %s: %O\", name, error);\n out.failed.push({ name, error });\n }\n }\n return out;\n }\n\n private resolveUrl(endpoint: McpEndpointConfig): string {\n if (\n endpoint.url.startsWith(\"http://\") ||\n endpoint.url.startsWith(\"https://\")\n ) {\n return endpoint.url;\n }\n return `${this.workspaceHost}${endpoint.url}`;\n }\n\n async connect(endpoint: McpEndpointConfig): Promise<void> {\n const resolvedUrl = this.resolveUrl(endpoint);\n const check = checkMcpUrl(resolvedUrl, this.policy);\n if (!check.ok) {\n throw new Error(\n `MCP endpoint '${endpoint.name}' refused at connect: ${check.reason}`,\n );\n }\n await assertResolvedHostSafe(\n check.url.hostname,\n this.policy,\n this.options.dnsLookup,\n );\n\n logger.info(\n \"Connecting to MCP server: %s at %s (forwardWorkspaceAuth=%s)\",\n endpoint.name,\n resolvedUrl,\n check.forwardWorkspaceAuth,\n );\n\n const initResponse = await this.sendRpc(\n resolvedUrl,\n \"initialize\",\n {\n protocolVersion: \"2025-03-26\",\n capabilities: {},\n clientInfo: { name: \"appkit-agent\", version: \"0.1.0\" },\n },\n { forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n\n if (initResponse.sessionId) {\n this.sessionIds.set(endpoint.name, initResponse.sessionId);\n }\n const sessionId = this.sessionIds.get(endpoint.name);\n\n await this.sendNotification(resolvedUrl, \"notifications/initialized\", {\n sessionId,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n });\n\n const listResponse = await this.sendRpc(\n resolvedUrl,\n \"tools/list\",\n {},\n { sessionId, forwardWorkspaceAuth: check.forwardWorkspaceAuth },\n );\n const toolList =\n (listResponse.result as { tools?: McpToolSchema[] })?.tools ?? [];\n\n const tools = new Map<string, McpToolSchema>();\n for (const tool of toolList) {\n tools.set(tool.name, tool);\n }\n\n this.connections.set(endpoint.name, {\n config: endpoint,\n resolvedUrl,\n forwardWorkspaceAuth: check.forwardWorkspaceAuth,\n tools,\n });\n logger.info(\n \"Connected to MCP server %s: %d tools available\",\n endpoint.name,\n tools.size,\n );\n }\n\n getAllToolDefinitions(): AgentToolDefinition[] {\n const defs: AgentToolDefinition[] = [];\n for (const [serverName, conn] of this.connections) {\n for (const [toolName, schema] of conn.tools) {\n defs.push({\n name: `mcp.${serverName}.${toolName}`,\n description: schema.description ?? toolName,\n parameters: coerceToolParameters(schema.inputSchema),\n });\n }\n }\n return defs;\n }\n\n /**\n * Whether the named MCP server may receive workspace-scoped auth headers\n * (e.g., an OBO bearer token from an end-user request). Callers should gate\n * auth-forwarding decisions on this to prevent credential exfiltration to\n * non-workspace hosts.\n */\n canForwardWorkspaceAuth(serverName: string): boolean {\n return this.connections.get(serverName)?.forwardWorkspaceAuth ?? false;\n }\n\n async callTool(\n qualifiedName: string,\n args: unknown,\n authHeaders?: Record<string, string>,\n callerSignal?: AbortSignal,\n ): Promise<string> {\n const parts = qualifiedName.split(\".\");\n if (parts.length < 3 || parts[0] !== \"mcp\") {\n throw new Error(`Invalid MCP tool name: ${qualifiedName}`);\n }\n const serverName = parts[1];\n const toolName = parts.slice(2).join(\".\");\n\n const conn = this.connections.get(serverName);\n if (!conn) {\n throw new Error(`MCP server not connected: ${serverName}`);\n }\n\n const sessionId = this.sessionIds.get(serverName);\n // authHeaders are caller-supplied credentials (typically the OBO token).\n // Only honor them if the destination URL was admitted with\n // forwardWorkspaceAuth=true at connect time.\n const scopedAuthOverride = conn.forwardWorkspaceAuth\n ? authHeaders\n : undefined;\n\n const rpcResult = await this.sendRpc(\n conn.resolvedUrl,\n \"tools/call\",\n { name: toolName, arguments: args },\n {\n authOverride: scopedAuthOverride,\n sessionId,\n forwardWorkspaceAuth: conn.forwardWorkspaceAuth,\n callerSignal,\n },\n );\n const result = rpcResult.result as McpToolCallResult;\n\n // `text` is optional on `McpToolCallResult.content[]` per the MCP\n // spec; filtering only on `type === \"text\"` lets `c.text` be\n // `undefined`, which `Array.join` would render as the literal\n // string `\"undefined\"` and ship to the agent. Narrow on both\n // fields so the joined string only contains real text.\n const textContent = (result.content ?? []).filter(\n (c): c is { type: \"text\"; text: string } =>\n c.type === \"text\" && typeof c.text === \"string\",\n );\n\n if (result.isError) {\n const errText = textContent.map((c) => c.text).join(\"\\n\");\n throw new Error(errText || \"MCP tool call failed\");\n }\n\n return textContent.map((c) => c.text).join(\"\\n\");\n }\n\n async close(): Promise<void> {\n this.closed = true;\n this.connections.clear();\n this.sessionIds.clear();\n }\n\n private async sendRpc(\n url: string,\n method: string,\n params?: Record<string, unknown>,\n options?: {\n authOverride?: Record<string, string>;\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n /**\n * Optional external abort signal (typically the agent's stream signal).\n * Composed with the built-in 30 s timeout so `/cancel` or agent-run\n * shutdown immediately propagates to the MCP fetch rather than waiting\n * for the remote server to respond.\n */\n callerSignal?: AbortSignal;\n },\n ): Promise<{ result: unknown; sessionId?: string }> {\n if (this.closed) throw new Error(\"MCP client is closed\");\n\n const request: JsonRpcRequest = {\n jsonrpc: \"2.0\",\n id: ++this.requestId,\n method,\n ...(params && { params }),\n };\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"User-Agent\": APPKIT_USER_AGENT,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n const signals: AbortSignal[] = [AbortSignal.timeout(30_000)];\n if (options?.callerSignal) signals.push(options.callerSignal);\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify(request),\n signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],\n });\n\n if (!response.ok) {\n throw new Error(\n `MCP request to ${method} failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n // Always read the body via the capped helper so a misconfigured or\n // malicious server can't exhaust client memory by streaming an\n // unbounded payload. Applies to both SSE (`response.text()` would\n // have buffered the whole stream) and plain JSON (`response.json()`\n // does the same internally).\n const bodyText = await readResponseTextCapped(\n response,\n MCP_RESPONSE_BODY_LIMIT_BYTES,\n method,\n );\n let json: JsonRpcResponse;\n\n if (contentType.includes(\"text/event-stream\")) {\n const lastData = bodyText\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data: \"))\n .map((line) => line.slice(6))\n .pop();\n if (!lastData) {\n throw new Error(`MCP SSE response for ${method} contained no data`);\n }\n json = JSON.parse(lastData) as JsonRpcResponse;\n } else {\n if (bodyText.length === 0) {\n throw new Error(`MCP response for ${method} had an empty body`);\n }\n json = JSON.parse(bodyText) as JsonRpcResponse;\n }\n\n if (json.error) {\n throw new Error(`MCP error (${json.error.code}): ${json.error.message}`);\n }\n\n const sid = response.headers.get(\"mcp-session-id\") ?? undefined;\n return { result: json.result, sessionId: sid };\n }\n\n private async sendNotification(\n url: string,\n method: string,\n options?: {\n sessionId?: string;\n forwardWorkspaceAuth?: boolean;\n },\n ): Promise<void> {\n if (this.closed) return;\n\n const authHeaders = await this.resolveAuthHeaders(options);\n const headers: Record<string, string> = {\n \"User-Agent\": APPKIT_USER_AGENT,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n ...authHeaders,\n };\n if (options?.sessionId) {\n headers[\"Mcp-Session-Id\"] = options.sessionId;\n }\n\n const fetchImpl = this.options.fetchImpl ?? fetch;\n // MCP notifications are fire-and-forget per spec — we don't throw on\n // failure. But silently swallowing 4xx/5xx hides server-side\n // rejections that would otherwise look like a successful connect()\n // followed by mysterious tool-call failures. Surface the bad status\n // via the logger so the dev sees it without breaking the protocol\n // contract.\n try {\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ jsonrpc: \"2.0\", method }),\n signal: AbortSignal.timeout(30_000),\n });\n if (!response.ok) {\n logger.warn(\n \"MCP notification %s to %s returned %d %s — the server may have rejected the request, but per MCP spec notifications are fire-and-forget and the connection is considered established.\",\n method,\n url,\n response.status,\n response.statusText,\n );\n }\n } catch (err) {\n logger.warn(\n \"MCP notification %s to %s failed before a response was received: %O\",\n method,\n url,\n err,\n );\n }\n }\n\n /**\n * Return the auth headers to send on an outbound request. Workspace auth\n * (SP or OBO) is only resolved when `forwardWorkspaceAuth` is true; for\n * non-workspace hosts no bearer token is attached.\n */\n private async resolveAuthHeaders(options?: {\n authOverride?: Record<string, string>;\n forwardWorkspaceAuth?: boolean;\n }): Promise<Record<string, string>> {\n if (!options?.forwardWorkspaceAuth) return {};\n if (options.authOverride) return options.authOverride;\n return this.authenticate();\n }\n}\n"],"mappings":";;;;;AAoCA,MAAM,SAAS,aAAa,gBAAgB;;;;;;;;;AAU5C,MAAM,gCAAgC,OAAO;;;;;;;;;;;;;AAc7C,MAAM,wBAA2D;CAC/D,MAAM;CACN,YAAY,EAAE;CACf;;;;;;;;;;AAWD,SAAS,qBACP,aACmC;AACnC,KAAI,CAAC,eAAe,OAAO,gBAAgB,SACzC,QAAO;CAET,MAAM,EAAE,MAAM,eAAe;AAC7B,KAAI,SAAS,SAAU,QAAO;AAC9B,KACE,eAAe,WACd,OAAO,eAAe,YACrB,eAAe,QACf,MAAM,QAAQ,WAAW,EAE3B,QAAO;AAET,QAAO;;AAGT,eAAe,uBACb,UACA,UACA,cACiB;AACjB,KAAI,CAAC,SAAS,KAAM,QAAO;CAC3B,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,MAAM,UAAU,IAAI,YAAY,QAAQ;CACxC,IAAI,QAAQ;CACZ,IAAI,MAAM;AACV,KAAI;AACF,SAAO,MAAM;GACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAC3C,OAAI,KAAM;AACV,YAAS,MAAM;AACf,OAAI,QAAQ,SACV,OAAM,IAAI,MACR,OAAO,aAAa,2BAA2B,SAAS,sEACzD;AAEH,UAAO,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;;AAEhD,SAAO,QAAQ,QAAQ;WACf;AACR,SAAO,aAAa;;AAEtB,QAAO;;;;;;;;;;;;;;;;;;AAkET,IAAa,kBAAb,MAA6B;CAC3B,AAAQ,8BAAc,IAAI,KAAkC;CAC5D,AAAQ,6BAAa,IAAI,KAAqB;CAC9C,AAAQ,YAAY;CACpB,AAAQ,SAAS;CAEjB,YACE,AAAQ,eACR,AAAQ,cACR,AAAQ,QACR,AAAQ,UAA+D,EAAE,EACzE;EAJQ;EACA;EACA;EACA;;;;;;;;;;;;;;;CAgBV,MAAM,WACJ,WAC8B;EAC9B,MAAM,UAAU,MAAM,QAAQ,WAC5B,UAAU,KAAK,OAAO,KAAK,QAAQ,GAAG,CAAC,CACxC;EACD,MAAM,MAA2B;GAAE,WAAW,EAAE;GAAE,QAAQ,EAAE;GAAE;AAC9D,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,IAAI,QAAQ;GAClB,MAAM,OAAO,UAAU,GAAG;AAC1B,OAAI,EAAE,WAAW,YACf,KAAI,UAAU,KAAK,KAAK;QACnB;IACL,MAAM,QACJ,EAAE,kBAAkB,QAAQ,EAAE,SAAS,IAAI,MAAM,OAAO,EAAE,OAAO,CAAC;AACpE,WAAO,MAAM,uCAAuC,MAAM,MAAM;AAChE,QAAI,OAAO,KAAK;KAAE;KAAM;KAAO,CAAC;;;AAGpC,SAAO;;CAGT,AAAQ,WAAW,UAAqC;AACtD,MACE,SAAS,IAAI,WAAW,UAAU,IAClC,SAAS,IAAI,WAAW,WAAW,CAEnC,QAAO,SAAS;AAElB,SAAO,GAAG,KAAK,gBAAgB,SAAS;;CAG1C,MAAM,QAAQ,UAA4C;EACxD,MAAM,cAAc,KAAK,WAAW,SAAS;EAC7C,MAAM,QAAQ,YAAY,aAAa,KAAK,OAAO;AACnD,MAAI,CAAC,MAAM,GACT,OAAM,IAAI,MACR,iBAAiB,SAAS,KAAK,wBAAwB,MAAM,SAC9D;AAEH,QAAM,uBACJ,MAAM,IAAI,UACV,KAAK,QACL,KAAK,QAAQ,UACd;AAED,SAAO,KACL,gEACA,SAAS,MACT,aACA,MAAM,qBACP;EAED,MAAM,eAAe,MAAM,KAAK,QAC9B,aACA,cACA;GACE,iBAAiB;GACjB,cAAc,EAAE;GAChB,YAAY;IAAE,MAAM;IAAgB,SAAS;IAAS;GACvD,EACD,EAAE,sBAAsB,MAAM,sBAAsB,CACrD;AAED,MAAI,aAAa,UACf,MAAK,WAAW,IAAI,SAAS,MAAM,aAAa,UAAU;EAE5D,MAAM,YAAY,KAAK,WAAW,IAAI,SAAS,KAAK;AAEpD,QAAM,KAAK,iBAAiB,aAAa,6BAA6B;GACpE;GACA,sBAAsB,MAAM;GAC7B,CAAC;EAQF,MAAM,YANe,MAAM,KAAK,QAC9B,aACA,cACA,EAAE,EACF;GAAE;GAAW,sBAAsB,MAAM;GAAsB,CAChE,EAEe,QAAwC,SAAS,EAAE;EAEnE,MAAM,wBAAQ,IAAI,KAA4B;AAC9C,OAAK,MAAM,QAAQ,SACjB,OAAM,IAAI,KAAK,MAAM,KAAK;AAG5B,OAAK,YAAY,IAAI,SAAS,MAAM;GAClC,QAAQ;GACR;GACA,sBAAsB,MAAM;GAC5B;GACD,CAAC;AACF,SAAO,KACL,kDACA,SAAS,MACT,MAAM,KACP;;CAGH,wBAA+C;EAC7C,MAAM,OAA8B,EAAE;AACtC,OAAK,MAAM,CAAC,YAAY,SAAS,KAAK,YACpC,MAAK,MAAM,CAAC,UAAU,WAAW,KAAK,MACpC,MAAK,KAAK;GACR,MAAM,OAAO,WAAW,GAAG;GAC3B,aAAa,OAAO,eAAe;GACnC,YAAY,qBAAqB,OAAO,YAAY;GACrD,CAAC;AAGN,SAAO;;;;;;;;CAST,wBAAwB,YAA6B;AACnD,SAAO,KAAK,YAAY,IAAI,WAAW,EAAE,wBAAwB;;CAGnE,MAAM,SACJ,eACA,MACA,aACA,cACiB;EACjB,MAAM,QAAQ,cAAc,MAAM,IAAI;AACtC,MAAI,MAAM,SAAS,KAAK,MAAM,OAAO,MACnC,OAAM,IAAI,MAAM,0BAA0B,gBAAgB;EAE5D,MAAM,aAAa,MAAM;EACzB,MAAM,WAAW,MAAM,MAAM,EAAE,CAAC,KAAK,IAAI;EAEzC,MAAM,OAAO,KAAK,YAAY,IAAI,WAAW;AAC7C,MAAI,CAAC,KACH,OAAM,IAAI,MAAM,6BAA6B,aAAa;EAG5D,MAAM,YAAY,KAAK,WAAW,IAAI,WAAW;EAIjD,MAAM,qBAAqB,KAAK,uBAC5B,cACA;EAaJ,MAAM,UAXY,MAAM,KAAK,QAC3B,KAAK,aACL,cACA;GAAE,MAAM;GAAU,WAAW;GAAM,EACnC;GACE,cAAc;GACd;GACA,sBAAsB,KAAK;GAC3B;GACD,CACF,EACwB;EAOzB,MAAM,eAAe,OAAO,WAAW,EAAE,EAAE,QACxC,MACC,EAAE,SAAS,UAAU,OAAO,EAAE,SAAS,SAC1C;AAED,MAAI,OAAO,SAAS;GAClB,MAAM,UAAU,YAAY,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AACzD,SAAM,IAAI,MAAM,WAAW,uBAAuB;;AAGpD,SAAO,YAAY,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;;CAGlD,MAAM,QAAuB;AAC3B,OAAK,SAAS;AACd,OAAK,YAAY,OAAO;AACxB,OAAK,WAAW,OAAO;;CAGzB,MAAc,QACZ,KACA,QACA,QACA,SAYkD;AAClD,MAAI,KAAK,OAAQ,OAAM,IAAI,MAAM,uBAAuB;EAExD,MAAM,UAA0B;GAC9B,SAAS;GACT,IAAI,EAAE,KAAK;GACX;GACA,GAAI,UAAU,EAAE,QAAQ;GACzB;EAED,MAAM,cAAc,MAAM,KAAK,mBAAmB,QAAQ;EAC1D,MAAM,UAAkC;GACtC,cAAc;GACd,gBAAgB;GAChB,QAAQ;GACR,GAAG;GACJ;AACD,MAAI,SAAS,UACX,SAAQ,oBAAoB,QAAQ;EAGtC,MAAM,YAAY,KAAK,QAAQ,aAAa;EAC5C,MAAM,UAAyB,CAAC,YAAY,QAAQ,IAAO,CAAC;AAC5D,MAAI,SAAS,aAAc,SAAQ,KAAK,QAAQ,aAAa;EAC7D,MAAM,WAAW,MAAM,UAAU,KAAK;GACpC,QAAQ;GACR;GACA,MAAM,KAAK,UAAU,QAAQ;GAC7B,QAAQ,QAAQ,SAAS,IAAI,YAAY,IAAI,QAAQ,GAAG,QAAQ;GACjE,CAAC;AAEF,MAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MACR,kBAAkB,OAAO,WAAW,SAAS,OAAO,GAAG,SAAS,aACjE;EAGH,MAAM,cAAc,SAAS,QAAQ,IAAI,eAAe,IAAI;EAM5D,MAAM,WAAW,MAAM,uBACrB,UACA,+BACA,OACD;EACD,IAAI;AAEJ,MAAI,YAAY,SAAS,oBAAoB,EAAE;GAC7C,MAAM,WAAW,SACd,MAAM,KAAK,CACX,QAAQ,SAAS,KAAK,WAAW,SAAS,CAAC,CAC3C,KAAK,SAAS,KAAK,MAAM,EAAE,CAAC,CAC5B,KAAK;AACR,OAAI,CAAC,SACH,OAAM,IAAI,MAAM,wBAAwB,OAAO,oBAAoB;AAErE,UAAO,KAAK,MAAM,SAAS;SACtB;AACL,OAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MAAM,oBAAoB,OAAO,oBAAoB;AAEjE,UAAO,KAAK,MAAM,SAAS;;AAG7B,MAAI,KAAK,MACP,OAAM,IAAI,MAAM,cAAc,KAAK,MAAM,KAAK,KAAK,KAAK,MAAM,UAAU;EAG1E,MAAM,MAAM,SAAS,QAAQ,IAAI,iBAAiB,IAAI;AACtD,SAAO;GAAE,QAAQ,KAAK;GAAQ,WAAW;GAAK;;CAGhD,MAAc,iBACZ,KACA,QACA,SAIe;AACf,MAAI,KAAK,OAAQ;EAEjB,MAAM,cAAc,MAAM,KAAK,mBAAmB,QAAQ;EAC1D,MAAM,UAAkC;GACtC,cAAc;GACd,gBAAgB;GAChB,QAAQ;GACR,GAAG;GACJ;AACD,MAAI,SAAS,UACX,SAAQ,oBAAoB,QAAQ;EAGtC,MAAM,YAAY,KAAK,QAAQ,aAAa;AAO5C,MAAI;GACF,MAAM,WAAW,MAAM,UAAU,KAAK;IACpC,QAAQ;IACR;IACA,MAAM,KAAK,UAAU;KAAE,SAAS;KAAO;KAAQ,CAAC;IAChD,QAAQ,YAAY,QAAQ,IAAO;IACpC,CAAC;AACF,OAAI,CAAC,SAAS,GACZ,QAAO,KACL,yLACA,QACA,KACA,SAAS,QACT,SAAS,WACV;WAEI,KAAK;AACZ,UAAO,KACL,uEACA,QACA,KACA,IACD;;;;;;;;CASL,MAAc,mBAAmB,SAGG;AAClC,MAAI,CAAC,SAAS,qBAAsB,QAAO,EAAE;AAC7C,MAAI,QAAQ,aAAc,QAAO,QAAQ;AACzC,SAAO,KAAK,cAAc"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/sql-warehouse/client.ts"],"sourcesContent":["import type { TelemetryOptions } from \"shared\";\nimport {\n AppKitError,\n ConfigurationError,\n ConnectionError,\n ExecutionError,\n ValidationError,\n} from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n ArrowStreamProcessor,\n type RefreshChunkLink,\n} from \"../../stream/arrow-stream-processor\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport {\n Context,\n type sql,\n type WorkspaceClient,\n} from \"../../workspace-client\";\nimport { buildEmptyArrowIPCBase64 } from \"./arrow-schema\";\nimport { executeStatementDefaults } from \"./defaults\";\nimport { WarehousePollBackoff } from \"./warehouse-poll-backoff\";\nimport { WarehouseStatusEmitter } from \"./warehouse-status-emitter\";\n\nconst logger = createLogger(\"connectors:sql-warehouse\");\n\n/**\n * Real column names from a statement's result manifest (positional\n * `column_N` fallback for any blank ones). Databricks encodes ARROW_STREAM\n * schemas positionally (`col_0`, …); these names let consumers relabel an\n * Arrow result to match the JSON path. Returns `undefined` when the manifest\n * carries no columns.\n */\nfunction arrowColumnNames(\n response: sql.StatementResponse,\n): string[] | undefined {\n const cols = response.manifest?.schema?.columns;\n if (!cols || cols.length === 0) return undefined;\n return cols.map((c, i) =>\n c.name && c.name.length > 0 ? c.name : `column_${i}`,\n );\n}\n\n/**\n * Maximum size for inline Arrow IPC attachments (25 MiB decoded — the\n * Databricks Statement Execution API hard cap on INLINE responses).\n *\n * The analytics route streams the decoded attachment straight back on the\n * query response body; this cap bounds the single in-memory copy. Larger\n * results fall through to `disposition: \"EXTERNAL_LINKS\"`, which streams\n * chunk-by-chunk and is not bound by this limit.\n */\nconst MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024;\n\n/**\n * Safety cap on how many additional EXTERNAL_LINKS chunks\n * {@link SQLWarehouseConnector._resolveAllExternalLinks} will follow when the\n * manifest omits `total_chunk_count`. High enough to cover any real result;\n * only bounds a misbehaving warehouse with a cyclic `next_chunk_index`.\n */\nconst MAX_EXTERNAL_CHUNK_FOLLOWS = 10_000;\n\ninterface SQLWarehouseConfig {\n timeout?: number;\n telemetry?: TelemetryOptions;\n}\n\n/**\n * Default ceiling for how long {@link SQLWarehouseConnector.ensureWarehouseRunning}\n * will wait for a warehouse to reach the RUNNING state before giving up.\n *\n * Five minutes covers a cold-start of a classic warehouse on most workspaces;\n * serverless typically reaches RUNNING within ~30s.\n */\nexport const DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Window during which a recent `RUNNING` observation lets subsequent calls\n * to {@link SQLWarehouseConnector.ensureWarehouseRunning} short-circuit\n * without making any SDK calls. Sized to roughly the upper bound of how\n * long Databricks keeps a warehouse \"stickily\" available between requests\n * — past that, we re-verify.\n */\nconst WAREHOUSE_RUNNING_CACHE_TTL_MS = 30_000;\n\n/**\n * A single observation of the warehouse state, emitted by\n * {@link SQLWarehouseConnector.ensureWarehouseRunning} so callers can stream\n * progress to clients (e.g. over SSE).\n *\n * Note: `health.summary` from the SDK is intentionally NOT exposed on this\n * type. It's free-form operator-oriented diagnostic text (cluster IDs,\n * capacity-failure reasons, internal RPC errors) that must not reach end\n * users. The raw value stays in the OTel span attributes for server-side\n * debugging only.\n */\nexport interface WarehouseStatusUpdate {\n /** Current state from the SDK (RUNNING | STARTING | STOPPED | STOPPING | DELETED | DELETING). */\n state: sql.State;\n /** Milliseconds elapsed since `ensureWarehouseRunning` was called. */\n elapsedMs: number;\n /** 1-based attempt counter — useful for tests and telemetry. */\n attempt: number;\n}\n\n/** Options for {@link SQLWarehouseConnector.ensureWarehouseRunning}. */\ninterface EnsureWarehouseRunningOptions {\n /** Invoked every time the warehouse state changes during the wait. */\n onStatus: (update: WarehouseStatusUpdate) => void;\n /** Aborts the wait. The connector treats abort as `ExecutionError.canceled()`. */\n signal?: AbortSignal;\n /** Hard ceiling on the total wait. Defaults to {@link DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS}. */\n timeoutMs?: number;\n /**\n * When `true` (default), a `STOPPED` warehouse is auto-started.\n * When `false`, `STOPPED` surfaces as `ConfigurationError` immediately —\n * useful for cost-controlled deployments that don't want any caller to\n * trigger billable warehouse starts.\n */\n autoStart?: boolean;\n /** @internal Fired once when `warehouses.start` is issued for this poll. */\n onWarehouseStartIssued?: () => void;\n}\n\n/** In-flight warehouse readiness work shared across concurrent callers. */\ninterface WarehouseReadinessInFlight {\n promise: Promise<void>;\n refCount: number;\n sharedController: AbortController;\n subscribers: Set<(update: WarehouseStatusUpdate) => void>;\n /** Last emitted update; replayed to late joiners. */\n lastUpdate: WarehouseStatusUpdate | null;\n /** `true` once this poll has called `warehouses.start`. */\n warehouseStartIssued: boolean;\n}\n\nexport class SQLWarehouseConnector {\n private readonly name = \"sql-warehouse\";\n\n private config: SQLWarehouseConfig;\n\n // Lazy-initialized: only created when Arrow format is used\n private _arrowProcessor: ArrowStreamProcessor | null = null;\n\n /**\n * Per-warehouse cache of the last RUNNING observation timestamp. Used by\n * {@link ensureWarehouseRunning} to short-circuit warm-path callers; see\n * {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}.\n */\n private _recentlyRunning = new Map<string, number>();\n /** Per-warehouse readiness singleflight — one poll loop per cold start. */\n private _readinessInFlight = new Map<string, WarehouseReadinessInFlight>();\n // telemetry\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n queryCount: Counter;\n queryDuration: Histogram;\n };\n\n constructor(config: SQLWarehouseConfig) {\n this.config = config;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n queryCount: this.telemetry.getMeter().createCounter(\"query.count\", {\n description: \"Total number of queries executed\",\n unit: \"1\",\n }),\n queryDuration: this.telemetry\n .getMeter()\n .createHistogram(\"query.duration\", {\n description: \"Duration of queries executed\",\n unit: \"ms\",\n }),\n };\n }\n\n /**\n * Lazily initializes and returns the ArrowStreamProcessor.\n * Only created on first Arrow format query to avoid unnecessary allocation.\n */\n private get arrowProcessor(): ArrowStreamProcessor {\n if (!this._arrowProcessor) {\n this._arrowProcessor = new ArrowStreamProcessor({\n timeout: this.config.timeout || executeStatementDefaults.timeout,\n retries: ArrowStreamProcessor.DEFAULT_RETRIES,\n });\n }\n return this._arrowProcessor;\n }\n\n async executeStatement(\n workspaceClient: WorkspaceClient,\n input: sql.ExecuteStatementRequest,\n signal?: AbortSignal,\n ) {\n const startTime = Date.now();\n let success = false;\n\n // if signal is aborted, throw an error\n if (signal?.aborted) {\n throw ExecutionError.canceled();\n }\n\n return this.telemetry.startActiveSpan(\n \"sql.query\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": input.warehouse_id || \"\",\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n \"db.has_parameters\": !!input.parameters,\n },\n },\n async (span: Span) => {\n let abortHandler: (() => void) | undefined;\n let isAborted = false;\n\n if (signal) {\n abortHandler = () => {\n // abort span if not recording\n if (!span.isRecording()) return;\n isAborted = true;\n span.setAttribute(\"cancelled\", true);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: \"Query cancelled by client\",\n });\n span.end();\n };\n signal.addEventListener(\"abort\", abortHandler, { once: true });\n }\n\n try {\n // validate required fields\n if (!input.statement) {\n throw ValidationError.missingField(\"statement\");\n }\n\n if (!input.warehouse_id) {\n throw ValidationError.missingField(\"warehouse_id\");\n }\n\n const body: sql.ExecuteStatementRequest = {\n statement: input.statement,\n parameters: input.parameters,\n warehouse_id: input.warehouse_id,\n catalog: input.catalog,\n schema: input.schema,\n wait_timeout:\n input.wait_timeout || executeStatementDefaults.wait_timeout,\n disposition:\n input.disposition || executeStatementDefaults.disposition,\n format: input.format || executeStatementDefaults.format,\n byte_limit: input.byte_limit,\n row_limit: input.row_limit,\n on_wait_timeout:\n input.on_wait_timeout || executeStatementDefaults.on_wait_timeout,\n };\n\n span.addEvent(\"statement.submitting\", {\n \"db.warehouse_id\": input.warehouse_id,\n });\n\n const response =\n await workspaceClient.statementExecution.executeStatement(\n body,\n this._createContext(signal),\n );\n\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n const status = response.status;\n const statementId = response.statement_id as string;\n\n span.setAttribute(\"db.statement_id\", statementId);\n span.addEvent(\"statement.submitted\", {\n \"db.statement_id\": response.statement_id,\n \"db.status\": status?.state,\n });\n\n let result:\n | sql.StatementResponse\n | { result: { statement_id: string; status: sql.StatementStatus } };\n\n switch (status?.state) {\n case \"RUNNING\":\n case \"PENDING\":\n span.addEvent(\"statement.polling_started\", {\n \"db.status\": response.status?.state,\n });\n result = await this._pollForStatementResult(\n workspaceClient,\n statementId,\n this.config.timeout,\n signal,\n );\n break;\n case \"SUCCEEDED\":\n result = await this._transformDataArray(\n response,\n workspaceClient,\n signal,\n );\n break;\n case \"FAILED\":\n throw ExecutionError.statementFailed(\n status.error?.message,\n status.error?.error_code,\n );\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n const resultData = result.result as any;\n const rowCount =\n resultData?.data?.length ?? resultData?.data_array?.length ?? 0;\n\n if (rowCount > 0) {\n span.setAttribute(\"db.result.row_count\", rowCount);\n }\n\n const duration = Date.now() - startTime;\n logger.event()?.setContext(\"sql-warehouse\", {\n warehouse_id: input.warehouse_id,\n rows_returned: rowCount,\n query_duration_ms: duration,\n });\n\n success = true;\n // only set success status if not aborted\n if (!isAborted) {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n return result;\n } catch (error) {\n // only record error if not already handled by abort\n if (!isAborted) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n logger.error(\n \"Statement execution failed: %s\",\n error instanceof Error ? error.message : String(error),\n );\n }\n\n this._rethrowStatementError(error);\n } finally {\n // remove abort handler\n if (abortHandler && signal) {\n signal.removeEventListener(\"abort\", abortHandler);\n }\n\n const duration = Date.now() - startTime;\n\n // end span if not already ended by abort handler\n if (!isAborted) {\n span.end();\n }\n\n const attributes = {\n \"db.warehouse_id\": input.warehouse_id,\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n success: success.toString(),\n };\n\n this.telemetryMetrics.queryCount.add(1, attributes);\n this.telemetryMetrics.queryDuration.record(duration, attributes);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait until the SQL warehouse is in the `RUNNING` state, auto-starting it\n * if currently `STOPPED`. Emits a {@link WarehouseStatusUpdate} whenever\n * the observed state changes, so callers can surface progress (e.g. over\n * SSE) instead of letting the UI freeze on a cold warehouse. Equal\n * successive observations are de-duplicated.\n *\n * Fast path: if this connector recently observed the warehouse RUNNING\n * (within {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}), the call returns\n * immediately without any SDK round-trip or status emission. This keeps\n * cache-hit analytics requests off the Databricks control plane.\n *\n * Concurrent callers for the same `warehouseId` share a single in-flight\n * poll loop (singleflight): only the owner issues `get`/`start`; joiners\n * receive broadcast `onStatus` updates and share the result promise.\n * When every waiter disconnects before `warehouses.start`, the shared poll\n * is aborted on the next microtask if still unclaimed (kills true orphans\n * without a fixed grace window). After `start` is issued, the poll runs to\n * completion even with no waiters.\n *\n * Behaviour by initial state:\n * - `RUNNING`: emits one update, caches the observation, returns.\n * - `STOPPED`: emits a synthetic `STARTING` update, calls\n * `workspaceClient.warehouses.start`, then polls until `RUNNING`.\n * When `autoStart: false`, throws `ConfigurationError` instead.\n * - `STARTING` / `STOPPING`: polls until `RUNNING`.\n * - `DELETED` / `DELETING`: throws `ConfigurationError.resourceNotFound`.\n *\n * Aborts and timeouts surface as `ExecutionError`. The poll loop uses\n * exponential backoff with ±15% jitter (1s → 30s cap) and runs inside a\n * `sql.warehouseReady` telemetry span.\n */\n async ensureWarehouseRunning(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: EnsureWarehouseRunningOptions,\n ): Promise<void> {\n const {\n onStatus,\n signal,\n timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n autoStart = true,\n } = opts;\n\n if (signal?.aborted) throw ExecutionError.canceled();\n if (!warehouseId) throw ValidationError.missingField(\"warehouse_id\");\n if (this._isRecentlyRunning(warehouseId)) return;\n\n const existing = this._readinessInFlight.get(warehouseId);\n if (existing && !existing.sharedController.signal.aborted) {\n return this._waitOnReadiness(existing, onStatus, signal, true);\n }\n\n const entry = this._spawnReadinessInFlight(workspaceClient, warehouseId, {\n timeoutMs,\n autoStart,\n });\n this._readinessInFlight.set(warehouseId, entry);\n return this._waitOnReadiness(entry, onStatus, signal, false);\n }\n\n private _waitOnReadiness(\n entry: WarehouseReadinessInFlight,\n onStatus: (update: WarehouseStatusUpdate) => void,\n signal: AbortSignal | undefined,\n join: boolean,\n ): Promise<void> {\n if (signal?.aborted) throw ExecutionError.canceled();\n if (join) entry.refCount++;\n entry.subscribers.add(onStatus);\n if (entry.lastUpdate) onStatus(entry.lastUpdate);\n return this._joinWarehouseReadiness(entry, signal);\n }\n\n private _spawnReadinessInFlight(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: { timeoutMs: number; autoStart: boolean },\n ): WarehouseReadinessInFlight {\n const sharedController = new AbortController();\n const entry: WarehouseReadinessInFlight = {\n refCount: 1,\n sharedController,\n subscribers: new Set(),\n lastUpdate: null,\n warehouseStartIssued: false,\n promise: Promise.resolve(),\n };\n\n entry.promise = this.telemetry\n .startActiveSpan(\n \"sql.warehouseReady\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": warehouseId,\n \"db.warehouse.startup_timeout_ms\": opts.timeoutMs,\n },\n },\n async (span: Span) => {\n try {\n await this._pollUntilWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n onStatus: (update) => this._broadcastReadiness(entry, update),\n signal: sharedController.signal,\n timeoutMs: opts.timeoutMs,\n autoStart: opts.autoStart,\n onWarehouseStartIssued: () => {\n entry.warehouseStartIssued = true;\n },\n },\n span,\n );\n span.setStatus({ code: SpanStatusCode.OK });\n } catch (error) {\n this._throwSanitizedReadinessError(error, warehouseId, span);\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n )\n .finally(() => {\n if (this._readinessInFlight.get(warehouseId) === entry) {\n this._readinessInFlight.delete(warehouseId);\n }\n });\n\n entry.promise.catch(() => {});\n return entry;\n }\n\n private _broadcastReadiness(\n entry: WarehouseReadinessInFlight,\n update: WarehouseStatusUpdate,\n ): void {\n entry.lastUpdate = update;\n for (const subscriber of entry.subscribers) {\n try {\n subscriber(update);\n } catch {\n // One consumer must not block updates to the rest.\n }\n }\n }\n\n /** Ref-counted await; caller abort rejects locally. */\n private _joinWarehouseReadiness(\n entry: WarehouseReadinessInFlight,\n callerSignal?: AbortSignal,\n ): Promise<void> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<void>((resolve, reject) => {\n let done = false;\n const finish = (outcome: \"ok\" | \"abort\" | \"err\", error?: unknown) => {\n if (done) return;\n done = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n if (outcome === \"ok\") resolve();\n else if (outcome === \"abort\") reject(ExecutionError.canceled());\n else reject(error);\n };\n const onAbort = () => {\n this._releaseReadinessWaiter(entry);\n finish(\"abort\");\n };\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n entry.promise.then(\n () => finish(\"ok\"),\n (error) => finish(\"err\", error),\n );\n });\n }\n\n private _releaseReadinessWaiter(entry: WarehouseReadinessInFlight): void {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount > 0 || entry.sharedController.signal.aborted) return;\n\n if (entry.warehouseStartIssued) {\n // Start already issued — finish warming for the process.\n return;\n }\n\n // Defer abort to the next microtask so a synchronous StrictMode\n // remount can rejoin before the shared poll is cancelled.\n queueMicrotask(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\"all warehouse readiness waiters aborted\");\n }\n });\n }\n\n private async _pollUntilWarehouseRunning(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: EnsureWarehouseRunningOptions,\n span: Span,\n ): Promise<void> {\n const {\n onStatus,\n signal,\n timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n autoStart = true,\n onWarehouseStartIssued,\n } = opts;\n const startTime = Date.now();\n const emitter = new WarehouseStatusEmitter(span, startTime, onStatus);\n let didStart = false;\n const backoff = new WarehousePollBackoff();\n\n while (true) {\n if (signal?.aborted) throw ExecutionError.canceled();\n if (Date.now() - startTime > timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n\n const info = await workspaceClient.warehouses.get(\n { id: warehouseId },\n this._createContext(signal),\n );\n const state = info?.state;\n const summary = info?.health?.summary;\n\n switch (state) {\n case \"RUNNING\":\n emitter.emit(state, summary);\n this._recentlyRunning.set(warehouseId, Date.now());\n span.setAttribute(\"db.warehouse.attempts\", emitter.attempt);\n return;\n case \"DELETED\":\n case \"DELETING\":\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n `The configured SQL warehouse is ${state}. Update DATABRICKS_WAREHOUSE_ID to point at an active warehouse.`,\n );\n case \"STOPPED\":\n if (!autoStart) {\n throw new ConfigurationError(\n \"The configured SQL warehouse is STOPPED and analytics auto-start is disabled. Start the warehouse manually or set analytics.autoStartWarehouse=true.\",\n );\n }\n if (!didStart) {\n emitter.emit(\"STARTING\", summary);\n onWarehouseStartIssued?.();\n await workspaceClient.warehouses.start(\n { id: warehouseId },\n this._createContext(signal),\n );\n didStart = true;\n } else {\n emitter.emit(state, summary);\n }\n break;\n case \"STARTING\":\n case \"STOPPING\":\n emitter.emit(state, summary);\n break;\n default:\n throw ExecutionError.unknownState(String(state ?? \"unknown\"));\n }\n\n const sleepMs = backoff.next();\n if (Date.now() + sleepMs - startTime >= timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n await this._sleepRespectingAbort(sleepMs, signal);\n }\n }\n\n /**\n * `true` if this connector observed `warehouseId` in the RUNNING state\n * within the recent-cache TTL.\n */\n private _isRecentlyRunning(warehouseId: string): boolean {\n const observedAt = this._recentlyRunning.get(warehouseId);\n return (\n observedAt !== undefined &&\n Date.now() - observedAt < WAREHOUSE_RUNNING_CACHE_TTL_MS\n );\n }\n\n /**\n * Final landing for any error thrown by the readiness poll loop. Records\n * the original on the span and (at debug level) on the logger, then\n * rethrows either the structured AppKitError unchanged or a curated\n * ExecutionError — never the raw SDK message, which can contain operator\n * internals.\n *\n * The `logger.debug` covers environments where OTel isn't configured\n * (common in dev) so the raw error isn't lost just because no span\n * exporter is hooked up.\n */\n private _throwSanitizedReadinessError(\n error: unknown,\n warehouseId: string,\n span: Span,\n ): never {\n if (error instanceof AppKitError) {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.code });\n throw error;\n }\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n logger.debug(\n \"Warehouse readiness check raw error for %s: %O\",\n warehouseId,\n error,\n );\n const wrapped = ExecutionError.statementFailed(\n \"Warehouse readiness check failed\",\n );\n span.setStatus({ code: SpanStatusCode.ERROR, message: wrapped.code });\n throw wrapped;\n }\n\n /**\n * Sleep for `ms` milliseconds, but resolve early (and reject with\n * `ExecutionError.canceled()`) if `signal` aborts mid-sleep. Used by the\n * warehouse readiness loop so that client disconnects don't keep the loop\n * polling for the full interval.\n */\n private _sleepRespectingAbort(\n ms: number,\n signal?: AbortSignal,\n ): Promise<void> {\n if (signal?.aborted) return Promise.reject(ExecutionError.canceled());\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(ExecutionError.canceled());\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n private async _pollForStatementResult(\n workspaceClient: WorkspaceClient,\n statementId: string,\n timeout = executeStatementDefaults.timeout,\n signal?: AbortSignal,\n ) {\n return this.telemetry.startActiveSpan(\n \"sql.poll\",\n {\n attributes: {\n \"db.statement_id\": statementId,\n \"db.polling.timeout\": timeout,\n },\n },\n async (span: Span) => {\n try {\n const startTime = Date.now();\n let delay = 1000;\n const maxDelayBetweenPolls = 5000; // max 5 seconds between polls\n let pollCount = 0;\n\n while (true) {\n pollCount++;\n span.setAttribute(\"db.polling.current_attempt\", pollCount);\n\n // check if timeout exceeded\n const elapsedTime = Date.now() - startTime;\n if (elapsedTime > timeout) {\n const error = ExecutionError.statementFailed(\n `Polling timeout exceeded after ${timeout}ms (elapsed: ${elapsedTime}ms)`,\n );\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n if (signal?.aborted) {\n const error = ExecutionError.canceled();\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n span.addEvent(\"polling.attempt\", {\n \"poll.attempt\": pollCount,\n \"poll.delay_ms\": delay,\n \"poll.elapsed_ms\": elapsedTime,\n });\n\n const response =\n await workspaceClient.statementExecution.getStatement(\n {\n statement_id: statementId,\n },\n this._createContext(signal),\n );\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n\n const status = response.status;\n\n span.addEvent(\"polling.status_check\", {\n \"db.status\": status?.state,\n \"poll.attempt\": pollCount,\n });\n\n switch (status?.state) {\n case \"PENDING\":\n case \"RUNNING\":\n // continue polling\n break;\n case \"SUCCEEDED\":\n span.setAttribute(\"db.polling.attempts\", pollCount);\n span.setAttribute(\"db.polling.total_duration_ms\", elapsedTime);\n span.addEvent(\"polling.completed\", {\n \"poll.attempts\": pollCount,\n \"poll.duration_ms\": elapsedTime,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n return this._transformDataArray(\n response,\n workspaceClient,\n signal,\n );\n case \"FAILED\":\n throw ExecutionError.statementFailed(\n status.error?.message,\n status.error?.error_code,\n );\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n // continue polling after delay\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * 2, maxDelayBetweenPolls);\n }\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n // error logging is handled by executeStatement's catch block (gated on isAborted)\n this._rethrowStatementError(error);\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n private async _transformDataArray(\n response: sql.StatementResponse,\n workspaceClient: WorkspaceClient,\n signal?: AbortSignal,\n ) {\n if (response.manifest?.format === \"ARROW_STREAM\") {\n const result = response.result as\n | (sql.ResultData & { attachment?: string })\n | undefined;\n\n // Inline Arrow: pass the base64 IPC attachment through unmodified so\n // the analytics route can stream it to the client, where the existing\n // ArrowClient infrastructure decodes it into a Table. Validate size\n // here to fail fast on runaway payloads.\n if (result?.attachment) {\n return this._validateArrowAttachment(response, result.attachment);\n }\n\n // External links: data fetched separately via statement_id. An empty\n // array is a zero-row result (some warehouses emit `external_links: []`\n // rather than omitting it) — it must NOT go down the streaming path\n // (`streamChunks([])` rejects), so fall through to synthesize an empty\n // Arrow table below.\n if (result?.external_links && result.external_links.length > 0) {\n return this.updateWithArrowStatus(response, workspaceClient, signal);\n }\n\n // Empty result with a known schema: synthesize a zero-row Arrow IPC\n // attachment so the client always receives an Arrow Table for\n // ARROW_STREAM, regardless of whether the warehouse returned data.\n // Note: an empty array (`data_array: []`) is truthy, so length-check\n // explicitly — otherwise zero-row responses fall through to the JSON\n // row transform below and return `[]` JSON rows instead of an Arrow\n // table.\n const hasNoRows =\n !result?.data_array ||\n (Array.isArray(result.data_array) && result.data_array.length === 0);\n if (hasNoRows && response.manifest?.schema?.columns) {\n const synthesized = buildEmptyArrowIPCBase64(\n response.manifest.schema.columns,\n );\n return {\n ...response,\n result: { ...(result ?? {}), attachment: synthesized },\n };\n }\n\n // Inline data_array under ARROW_STREAM (rare): fall through to the\n // row transform below. The hook will receive `type: \"result\"` rows;\n // callers asking for ARROW_STREAM should not hit this path with\n // current Databricks warehouses.\n }\n\n if (!response.result?.data_array || !response.manifest?.schema?.columns) {\n return response;\n }\n\n const columns = response.manifest.schema.columns;\n\n const transformedData = response.result.data_array.map((row) => {\n const obj: Record<string, unknown> = {};\n row.forEach((value, index) => {\n const column = columns[index];\n const columnName = column?.name || `column_${index}`;\n\n // attempt to parse JSON strings for string columns\n if (\n column?.type_name === \"STRING\" &&\n typeof value === \"string\" &&\n value &&\n (value[0] === \"{\" || value[0] === \"[\")\n ) {\n try {\n obj[columnName] = JSON.parse(value);\n } catch {\n // if parsing fails, keep as string\n obj[columnName] = value;\n }\n } else {\n obj[columnName] = value;\n }\n });\n return obj;\n });\n\n // remove data_array\n const { data_array: _data_array, ...restResult } = response.result;\n return {\n ...response,\n result: {\n ...restResult,\n data: transformedData,\n },\n };\n }\n\n /**\n * Validate a base64 Arrow IPC attachment (INLINE ARROW_STREAM) and attach\n * the real column names from the result manifest.\n *\n * Databricks warehouses encode the Arrow schema positionally (`col_0`, …);\n * the real, aliased names live only in `manifest.schema.columns`. Rather\n * than decode/re-encode the payload server-side, we carry the names on the\n * result (`columnNames`) so the route can hand them to the client in a\n * response header and the client relabels the decoded Table — the one\n * mechanism used for both INLINE and EXTERNAL_LINKS.\n */\n private _validateArrowAttachment(\n response: sql.StatementResponse,\n attachment: string,\n ) {\n // Cap the size to protect against unbounded inline payloads from\n // misbehaving warehouses. `MAX_INLINE_ATTACHMENT_BYTES` tracks the API's\n // ~25 MiB inline hard cap and bounds memory if a server returns a runaway\n // response.\n //\n // Strip whitespace (rare but legal in base64) and account for trailing\n // `=` padding so the byte count is exact rather than an upper bound.\n const stripped = attachment.replace(/\\s+/g, \"\");\n const padding = stripped.endsWith(\"==\")\n ? 2\n : stripped.endsWith(\"=\")\n ? 1\n : 0;\n const decodedSize = Math.floor((stripped.length * 3) / 4) - padding;\n if (decodedSize > MAX_INLINE_ATTACHMENT_BYTES) {\n throw ExecutionError.statementFailed(\n `Inline Arrow attachment exceeds maximum size (${decodedSize} > ${MAX_INLINE_ATTACHMENT_BYTES} bytes)`,\n );\n }\n\n const columnNames = arrowColumnNames(response);\n if (columnNames) {\n return {\n ...response,\n result: {\n ...(response.result as sql.ResultData & {\n attachment?: string;\n columnNames?: string[];\n }),\n // `statement_id` is a top-level field, not on `ResultData` — carry it\n // onto the result (as the EXTERNAL_LINKS path does) so the route can\n // advertise it in `X-Appkit-Arrow-Columns-Ref` for wide inline schemas.\n statement_id: response.statement_id,\n columnNames,\n },\n };\n }\n\n return response;\n }\n\n private async updateWithArrowStatus(\n response: sql.StatementResponse,\n workspaceClient: WorkspaceClient,\n signal?: AbortSignal,\n ): Promise<{\n result: {\n statement_id: string;\n status: sql.StatementStatus;\n columnNames?: string[];\n external_links?: sql.ExternalLink[];\n refreshChunkLink?: RefreshChunkLink;\n };\n }> {\n const statementId = response.statement_id as string;\n return {\n result: {\n statement_id: statementId,\n status: {\n state: response.status?.state,\n error: response.status?.error,\n } as sql.StatementStatus,\n columnNames: arrowColumnNames(response),\n // Resolve the pre-signed links for EVERY chunk in the caller's own\n // execution context. Streaming these directly (see\n // {@link streamExternalLinks}) avoids a second `getStatement` under\n // the ambient service-principal context — which would fail for\n // `.obo.sql` (user-owned) statements.\n external_links: await this._resolveAllExternalLinks(\n workspaceClient,\n statementId,\n response,\n signal,\n ),\n // Bound to this caller's identity so the streamer can re-mint an\n // expired chunk link mid-download (links live <= 15 min; a large/slow\n // result can outlast the earliest ones).\n refreshChunkLink: this._makeChunkLinkRefresher(\n workspaceClient,\n statementId,\n ),\n },\n };\n }\n\n /**\n * Resolve pre-signed links for EVERY chunk of an EXTERNAL_LINKS result.\n *\n * The execute/getStatement response carries only the first chunk's links\n * (each link, except the last, exposes `next_chunk_index`); the remaining\n * chunks are fetched with `getStatementResultChunkN`. Runs in the caller's\n * identity context (user creds for `.obo.sql`), so there is no cross-identity\n * fetch. Only the tiny link metadata is resolved eagerly — the bytes still\n * stream one chunk at a time downstream. Without this a multi-chunk result\n * would be silently truncated to its first chunk.\n */\n private async _resolveAllExternalLinks(\n workspaceClient: WorkspaceClient,\n statementId: string,\n response: sql.StatementResponse,\n signal?: AbortSignal,\n ): Promise<sql.ExternalLink[] | undefined> {\n const first = response.result?.external_links;\n if (!first || first.length === 0) return first;\n\n const links: sql.ExternalLink[] = [...first];\n // Bound the follow loop so a warehouse returning a cyclic/never-ending\n // `next_chunk_index` can't spin forever. The manifest's chunk count is the\n // natural bound; fall back to a generous safety cap if it's absent (real\n // results still terminate earlier when `next_chunk_index` becomes null) so\n // a missing count doesn't silently truncate a genuine multi-chunk result.\n const maxFetches =\n response.manifest?.total_chunk_count ?? MAX_EXTERNAL_CHUNK_FOLLOWS;\n let next = this._nextChunkIndex(first);\n for (let fetches = 0; next != null && fetches < maxFetches; fetches++) {\n if (signal?.aborted) throw ExecutionError.canceled();\n const chunk =\n await workspaceClient.statementExecution.getStatementResultChunkN(\n { statement_id: statementId, chunk_index: next },\n this._createContext(signal),\n );\n const chunkLinks = chunk.external_links ?? [];\n if (chunkLinks.length === 0) break;\n links.push(...chunkLinks);\n next = this._nextChunkIndex(chunkLinks);\n }\n return links;\n }\n\n /** The `next_chunk_index` advertised by a chunk's links, if any. */\n private _nextChunkIndex(links: sql.ExternalLink[]): number | undefined {\n for (const link of links) {\n if (link.next_chunk_index != null) return link.next_chunk_index;\n }\n return undefined;\n }\n\n /**\n * A closure that re-mints a single chunk's pre-signed link via\n * `getStatementResultChunkN`, bound to the caller's workspace client +\n * statement id. Created here (in the caller's identity context) so the\n * streamer — which runs outside that context — can refresh an expired link\n * for `.obo.sql` statements without a cross-identity `getStatement`.\n */\n private _makeChunkLinkRefresher(\n workspaceClient: WorkspaceClient,\n statementId: string,\n ): RefreshChunkLink {\n return async (chunkIndex, signal) => {\n const chunk =\n await workspaceClient.statementExecution.getStatementResultChunkN(\n { statement_id: statementId, chunk_index: chunkIndex },\n this._createContext(signal),\n );\n return chunk.external_links?.find((l) => l.chunk_index === chunkIndex);\n };\n }\n\n /**\n * Stream already-resolved EXTERNAL_LINKS chunks as raw Arrow IPC bytes, one\n * chunk in memory at a time. Takes the `external_links` the caller already\n * has from `executeStatement` (see {@link updateWithArrowStatus}), so there\n * is no extra `getStatement` round-trip and no execution-context mismatch —\n * the pre-signed URLs need no auth to download.\n */\n streamExternalLinks(\n chunks: sql.ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n return this.arrowProcessor.streamChunks(chunks, signal, refresh);\n }\n\n /**\n * Fetch just the real column names for a completed statement from its result\n * manifest — no result data. Backs the `/columns/:statementId` fallback the\n * client uses when a very wide schema's names exceed the response-header\n * size limit. Stateless: re-derives from the warehouse, no server cache.\n */\n async getColumnNames(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<string[] | undefined> {\n const response = await workspaceClient.statementExecution.getStatement(\n { statement_id: jobId },\n this._createContext(signal),\n );\n return arrowColumnNames(response);\n }\n\n /**\n * Normalize an error caught during statement execution/polling and rethrow.\n * Abort and already-structured `AppKitError`s pass through untouched;\n * anything else is wrapped as an `ExecutionError`, preserving the SDK's\n * structured `ApiError.errorCode` (e.g. \"INVALID_PARAMETER_VALUE\",\n * \"BAD_REQUEST\") so callers can branch on a stable identifier rather than\n * substring-matching the message.\n */\n private _rethrowStatementError(error: unknown): never {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw error;\n }\n if (error instanceof AppKitError) {\n throw error;\n }\n const sdkErrorCode =\n error && typeof error === \"object\" && \"errorCode\" in error\n ? (error as { errorCode?: unknown }).errorCode\n : undefined;\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n typeof sdkErrorCode === \"string\" ? sdkErrorCode : undefined,\n );\n }\n\n // create context for cancellation token\n private _createContext(signal?: AbortSignal) {\n return new Context({\n cancellationToken: {\n isCancellationRequested: signal?.aborted ?? false,\n onCancellationRequested: (cb: () => void) => {\n signal?.addEventListener(\"abort\", cb, { once: true });\n },\n },\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,MAAM,SAAS,aAAa,2BAA2B;;;;;;;;AASvD,SAAS,iBACP,UACsB;CACtB,MAAM,OAAO,SAAS,UAAU,QAAQ;AACxC,KAAI,CAAC,QAAQ,KAAK,WAAW,EAAG,QAAO;AACvC,QAAO,KAAK,KAAK,GAAG,MAClB,EAAE,QAAQ,EAAE,KAAK,SAAS,IAAI,EAAE,OAAO,UAAU,IAClD;;;;;;;;;;;AAYH,MAAM,8BAA8B,KAAK,OAAO;;;;;;;AAQhD,MAAM,6BAA6B;;;;;;;;AAcnC,MAAa,uCAAuC,MAAS;;;;;;;;AAS7D,MAAM,iCAAiC;AAqDvC,IAAa,wBAAb,MAAmC;CACjC,AAAiB,OAAO;CAExB,AAAQ;CAGR,AAAQ,kBAA+C;;;;;;CAOvD,AAAQ,mCAAmB,IAAI,KAAqB;;CAEpD,AAAQ,qCAAqB,IAAI,KAAyC;CAE1E,AAAiB;CACjB,AAAiB;CAKjB,YAAY,QAA4B;AACtC,OAAK,SAAS;AAEd,OAAK,YAAY,iBAAiB,YAChC,KAAK,MACL,KAAK,OAAO,UACb;AACD,OAAK,mBAAmB;GACtB,YAAY,KAAK,UAAU,UAAU,CAAC,cAAc,eAAe;IACjE,aAAa;IACb,MAAM;IACP,CAAC;GACF,eAAe,KAAK,UACjB,UAAU,CACV,gBAAgB,kBAAkB;IACjC,aAAa;IACb,MAAM;IACP,CAAC;GACL;;;;;;CAOH,IAAY,iBAAuC;AACjD,MAAI,CAAC,KAAK,gBACR,MAAK,kBAAkB,IAAI,qBAAqB;GAC9C,SAAS,KAAK,OAAO,WAAW,yBAAyB;GACzD,SAAS,qBAAqB;GAC/B,CAAC;AAEJ,SAAO,KAAK;;CAGd,MAAM,iBACJ,iBACA,OACA,QACA;EACA,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAGd,MAAI,QAAQ,QACV,OAAM,eAAe,UAAU;AAGjC,SAAO,KAAK,UAAU,gBACpB,aACA;GACE,MAAM,SAAS;GACf,YAAY;IACV,aAAa;IACb,mBAAmB,MAAM,gBAAgB;IACzC,cAAc,MAAM,WAAW;IAC/B,aAAa,MAAM,UAAU;IAC7B,gBAAgB,MAAM,WAAW,UAAU,GAAG,IAAI,IAAI;IACtD,qBAAqB,CAAC,CAAC,MAAM;IAC9B;GACF,EACD,OAAO,SAAe;GACpB,IAAI;GACJ,IAAI,YAAY;AAEhB,OAAI,QAAQ;AACV,yBAAqB;AAEnB,SAAI,CAAC,KAAK,aAAa,CAAE;AACzB,iBAAY;AACZ,UAAK,aAAa,aAAa,KAAK;AACpC,UAAK,UAAU;MACb,MAAM,eAAe;MACrB,SAAS;MACV,CAAC;AACF,UAAK,KAAK;;AAEZ,WAAO,iBAAiB,SAAS,cAAc,EAAE,MAAM,MAAM,CAAC;;AAGhE,OAAI;AAEF,QAAI,CAAC,MAAM,UACT,OAAM,gBAAgB,aAAa,YAAY;AAGjD,QAAI,CAAC,MAAM,aACT,OAAM,gBAAgB,aAAa,eAAe;IAGpD,MAAM,OAAoC;KACxC,WAAW,MAAM;KACjB,YAAY,MAAM;KAClB,cAAc,MAAM;KACpB,SAAS,MAAM;KACf,QAAQ,MAAM;KACd,cACE,MAAM,gBAAgB,yBAAyB;KACjD,aACE,MAAM,eAAe,yBAAyB;KAChD,QAAQ,MAAM,UAAU,yBAAyB;KACjD,YAAY,MAAM;KAClB,WAAW,MAAM;KACjB,iBACE,MAAM,mBAAmB,yBAAyB;KACrD;AAED,SAAK,SAAS,wBAAwB,EACpC,mBAAmB,MAAM,cAC1B,CAAC;IAEF,MAAM,WACJ,MAAM,gBAAgB,mBAAmB,iBACvC,MACA,KAAK,eAAe,OAAO,CAC5B;AAEH,QAAI,CAAC,SACH,OAAM,gBAAgB,WAAW,gBAAgB;IAEnD,MAAM,SAAS,SAAS;IACxB,MAAM,cAAc,SAAS;AAE7B,SAAK,aAAa,mBAAmB,YAAY;AACjD,SAAK,SAAS,uBAAuB;KACnC,mBAAmB,SAAS;KAC5B,aAAa,QAAQ;KACtB,CAAC;IAEF,IAAI;AAIJ,YAAQ,QAAQ,OAAhB;KACE,KAAK;KACL,KAAK;AACH,WAAK,SAAS,6BAA6B,EACzC,aAAa,SAAS,QAAQ,OAC/B,CAAC;AACF,eAAS,MAAM,KAAK,wBAClB,iBACA,aACA,KAAK,OAAO,SACZ,OACD;AACD;KACF,KAAK;AACH,eAAS,MAAM,KAAK,oBAClB,UACA,iBACA,OACD;AACD;KACF,KAAK,SACH,OAAM,eAAe,gBACnB,OAAO,OAAO,SACd,OAAO,OAAO,WACf;KACH,KAAK,WACH,OAAM,eAAe,UAAU;KACjC,KAAK,SACH,OAAM,eAAe,eAAe;KACtC,QACE,OAAM,eAAe,aACnB,OAAO,QAAQ,SAAS,UAAU,CACnC;;IAGL,MAAM,aAAa,OAAO;IAC1B,MAAM,WACJ,YAAY,MAAM,UAAU,YAAY,YAAY,UAAU;AAEhE,QAAI,WAAW,EACb,MAAK,aAAa,uBAAuB,SAAS;IAGpD,MAAM,WAAW,KAAK,KAAK,GAAG;AAC9B,WAAO,OAAO,EAAE,WAAW,iBAAiB;KAC1C,cAAc,MAAM;KACpB,eAAe;KACf,mBAAmB;KACpB,CAAC;AAEF,cAAU;AAEV,QAAI,CAAC,UACH,MAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAE7C,WAAO;YACA,OAAO;AAEd,QAAI,CAAC,WAAW;AACd,UAAK,gBAAgB,MAAe;AACpC,UAAK,UAAU;MACb,MAAM,eAAe;MACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;MAChE,CAAC;AAEF,YAAO,MACL,kCACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,CACvD;;AAGH,SAAK,uBAAuB,MAAM;aAC1B;AAER,QAAI,gBAAgB,OAClB,QAAO,oBAAoB,SAAS,aAAa;IAGnD,MAAM,WAAW,KAAK,KAAK,GAAG;AAG9B,QAAI,CAAC,UACH,MAAK,KAAK;IAGZ,MAAM,aAAa;KACjB,mBAAmB,MAAM;KACzB,cAAc,MAAM,WAAW;KAC/B,aAAa,MAAM,UAAU;KAC7B,gBAAgB,MAAM,WAAW,UAAU,GAAG,IAAI,IAAI;KACtD,SAAS,QAAQ,UAAU;KAC5B;AAED,SAAK,iBAAiB,WAAW,IAAI,GAAG,WAAW;AACnD,SAAK,iBAAiB,cAAc,OAAO,UAAU,WAAW;;KAGpE;GAAE,MAAM,KAAK;GAAM,eAAe;GAAM,CACzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCH,MAAM,uBACJ,iBACA,aACA,MACe;EACf,MAAM,EACJ,UACA,QACA,YAAY,sCACZ,YAAY,SACV;AAEJ,MAAI,QAAQ,QAAS,OAAM,eAAe,UAAU;AACpD,MAAI,CAAC,YAAa,OAAM,gBAAgB,aAAa,eAAe;AACpE,MAAI,KAAK,mBAAmB,YAAY,CAAE;EAE1C,MAAM,WAAW,KAAK,mBAAmB,IAAI,YAAY;AACzD,MAAI,YAAY,CAAC,SAAS,iBAAiB,OAAO,QAChD,QAAO,KAAK,iBAAiB,UAAU,UAAU,QAAQ,KAAK;EAGhE,MAAM,QAAQ,KAAK,wBAAwB,iBAAiB,aAAa;GACvE;GACA;GACD,CAAC;AACF,OAAK,mBAAmB,IAAI,aAAa,MAAM;AAC/C,SAAO,KAAK,iBAAiB,OAAO,UAAU,QAAQ,MAAM;;CAG9D,AAAQ,iBACN,OACA,UACA,QACA,MACe;AACf,MAAI,QAAQ,QAAS,OAAM,eAAe,UAAU;AACpD,MAAI,KAAM,OAAM;AAChB,QAAM,YAAY,IAAI,SAAS;AAC/B,MAAI,MAAM,WAAY,UAAS,MAAM,WAAW;AAChD,SAAO,KAAK,wBAAwB,OAAO,OAAO;;CAGpD,AAAQ,wBACN,iBACA,aACA,MAC4B;EAC5B,MAAM,mBAAmB,IAAI,iBAAiB;EAC9C,MAAM,QAAoC;GACxC,UAAU;GACV;GACA,6BAAa,IAAI,KAAK;GACtB,YAAY;GACZ,sBAAsB;GACtB,SAAS,QAAQ,SAAS;GAC3B;AAED,QAAM,UAAU,KAAK,UAClB,gBACC,sBACA;GACE,MAAM,SAAS;GACf,YAAY;IACV,aAAa;IACb,mBAAmB;IACnB,mCAAmC,KAAK;IACzC;GACF,EACD,OAAO,SAAe;AACpB,OAAI;AACF,UAAM,KAAK,2BACT,iBACA,aACA;KACE,WAAW,WAAW,KAAK,oBAAoB,OAAO,OAAO;KAC7D,QAAQ,iBAAiB;KACzB,WAAW,KAAK;KAChB,WAAW,KAAK;KAChB,8BAA8B;AAC5B,YAAM,uBAAuB;;KAEhC,EACD,KACD;AACD,SAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;YACpC,OAAO;AACd,SAAK,8BAA8B,OAAO,aAAa,KAAK;aACpD;AACR,SAAK,KAAK;;KAGd;GAAE,MAAM,KAAK;GAAM,eAAe;GAAM,CACzC,CACA,cAAc;AACb,OAAI,KAAK,mBAAmB,IAAI,YAAY,KAAK,MAC/C,MAAK,mBAAmB,OAAO,YAAY;IAE7C;AAEJ,QAAM,QAAQ,YAAY,GAAG;AAC7B,SAAO;;CAGT,AAAQ,oBACN,OACA,QACM;AACN,QAAM,aAAa;AACnB,OAAK,MAAM,cAAc,MAAM,YAC7B,KAAI;AACF,cAAW,OAAO;UACZ;;;CAOZ,AAAQ,wBACN,OACA,cACe;AACf,MAAI,CAAC,aAAc,QAAO,MAAM;AAEhC,SAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,IAAI,OAAO;GACX,MAAM,UAAU,SAAiC,UAAoB;AACnE,QAAI,KAAM;AACV,WAAO;AACP,iBAAa,oBAAoB,SAAS,QAAQ;AAClD,QAAI,YAAY,KAAM,UAAS;aACtB,YAAY,QAAS,QAAO,eAAe,UAAU,CAAC;QAC1D,QAAO,MAAM;;GAEpB,MAAM,gBAAgB;AACpB,SAAK,wBAAwB,MAAM;AACnC,WAAO,QAAQ;;AAEjB,gBAAa,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;AAC/D,SAAM,QAAQ,WACN,OAAO,KAAK,GACjB,UAAU,OAAO,OAAO,MAAM,CAChC;IACD;;CAGJ,AAAQ,wBAAwB,OAAyC;AACvE,MAAI,MAAM,WAAW,EAAG,OAAM;AAC9B,MAAI,MAAM,WAAW,KAAK,MAAM,iBAAiB,OAAO,QAAS;AAEjE,MAAI,MAAM,qBAER;AAKF,uBAAqB;AACnB,OAAI,MAAM,YAAY,KAAK,CAAC,MAAM,iBAAiB,OAAO,QACxD,OAAM,iBAAiB,MAAM,0CAA0C;IAEzE;;CAGJ,MAAc,2BACZ,iBACA,aACA,MACA,MACe;EACf,MAAM,EACJ,UACA,QACA,YAAY,sCACZ,YAAY,MACZ,2BACE;EACJ,MAAM,YAAY,KAAK,KAAK;EAC5B,MAAM,UAAU,IAAI,uBAAuB,MAAM,WAAW,SAAS;EACrE,IAAI,WAAW;EACf,MAAM,UAAU,IAAI,sBAAsB;AAE1C,SAAO,MAAM;AACX,OAAI,QAAQ,QAAS,OAAM,eAAe,UAAU;AACpD,OAAI,KAAK,KAAK,GAAG,YAAY,UAC3B,OAAM,eAAe,gBACnB,8CAA8C,UAAU,IACzD;GAGH,MAAM,OAAO,MAAM,gBAAgB,WAAW,IAC5C,EAAE,IAAI,aAAa,EACnB,KAAK,eAAe,OAAO,CAC5B;GACD,MAAM,QAAQ,MAAM;GACpB,MAAM,UAAU,MAAM,QAAQ;AAE9B,WAAQ,OAAR;IACE,KAAK;AACH,aAAQ,KAAK,OAAO,QAAQ;AAC5B,UAAK,iBAAiB,IAAI,aAAa,KAAK,KAAK,CAAC;AAClD,UAAK,aAAa,yBAAyB,QAAQ,QAAQ;AAC3D;IACF,KAAK;IACL,KAAK,WACH,OAAM,mBAAmB,iBACvB,gBACA,mCAAmC,MAAM,mEAC1C;IACH,KAAK;AACH,SAAI,CAAC,UACH,OAAM,IAAI,mBACR,uJACD;AAEH,SAAI,CAAC,UAAU;AACb,cAAQ,KAAK,YAAY,QAAQ;AACjC,gCAA0B;AAC1B,YAAM,gBAAgB,WAAW,MAC/B,EAAE,IAAI,aAAa,EACnB,KAAK,eAAe,OAAO,CAC5B;AACD,iBAAW;WAEX,SAAQ,KAAK,OAAO,QAAQ;AAE9B;IACF,KAAK;IACL,KAAK;AACH,aAAQ,KAAK,OAAO,QAAQ;AAC5B;IACF,QACE,OAAM,eAAe,aAAa,OAAO,SAAS,UAAU,CAAC;;GAGjE,MAAM,UAAU,QAAQ,MAAM;AAC9B,OAAI,KAAK,KAAK,GAAG,UAAU,aAAa,UACtC,OAAM,eAAe,gBACnB,8CAA8C,UAAU,IACzD;AAEH,SAAM,KAAK,sBAAsB,SAAS,OAAO;;;;;;;CAQrD,AAAQ,mBAAmB,aAA8B;EACvD,MAAM,aAAa,KAAK,iBAAiB,IAAI,YAAY;AACzD,SACE,eAAe,UACf,KAAK,KAAK,GAAG,aAAa;;;;;;;;;;;;;CAe9B,AAAQ,8BACN,OACA,aACA,MACO;AACP,MAAI,iBAAiB,aAAa;AAChC,QAAK,gBAAgB,MAAM;AAC3B,QAAK,UAAU;IAAE,MAAM,eAAe;IAAO,SAAS,MAAM;IAAM,CAAC;AACnE,SAAM;;AAER,OAAK,gBACH,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAC1D;AACD,SAAO,MACL,kDACA,aACA,MACD;EACD,MAAM,UAAU,eAAe,gBAC7B,mCACD;AACD,OAAK,UAAU;GAAE,MAAM,eAAe;GAAO,SAAS,QAAQ;GAAM,CAAC;AACrE,QAAM;;;;;;;;CASR,AAAQ,sBACN,IACA,QACe;AACf,MAAI,QAAQ,QAAS,QAAO,QAAQ,OAAO,eAAe,UAAU,CAAC;AACrE,SAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,MAAM,QAAQ,iBAAiB;AAC7B,YAAQ,oBAAoB,SAAS,QAAQ;AAC7C,aAAS;MACR,GAAG;GACN,MAAM,gBAAgB;AACpB,iBAAa,MAAM;AACnB,WAAO,eAAe,UAAU,CAAC;;AAEnC,WAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;IAC1D;;CAGJ,MAAc,wBACZ,iBACA,aACA,UAAU,yBAAyB,SACnC,QACA;AACA,SAAO,KAAK,UAAU,gBACpB,YACA,EACE,YAAY;GACV,mBAAmB;GACnB,sBAAsB;GACvB,EACF,EACD,OAAO,SAAe;AACpB,OAAI;IACF,MAAM,YAAY,KAAK,KAAK;IAC5B,IAAI,QAAQ;IACZ,MAAM,uBAAuB;IAC7B,IAAI,YAAY;AAEhB,WAAO,MAAM;AACX;AACA,UAAK,aAAa,8BAA8B,UAAU;KAG1D,MAAM,cAAc,KAAK,KAAK,GAAG;AACjC,SAAI,cAAc,SAAS;MACzB,MAAM,QAAQ,eAAe,gBAC3B,kCAAkC,QAAQ,eAAe,YAAY,KACtE;AACD,WAAK,gBAAgB,MAAM;AAC3B,WAAK,UAAU,EAAE,MAAM,eAAe,OAAO,CAAC;AAC9C,YAAM;;AAGR,SAAI,QAAQ,SAAS;MACnB,MAAM,QAAQ,eAAe,UAAU;AACvC,WAAK,gBAAgB,MAAM;AAC3B,WAAK,UAAU,EAAE,MAAM,eAAe,OAAO,CAAC;AAC9C,YAAM;;AAGR,UAAK,SAAS,mBAAmB;MAC/B,gBAAgB;MAChB,iBAAiB;MACjB,mBAAmB;MACpB,CAAC;KAEF,MAAM,WACJ,MAAM,gBAAgB,mBAAmB,aACvC,EACE,cAAc,aACf,EACD,KAAK,eAAe,OAAO,CAC5B;AACH,SAAI,CAAC,SACH,OAAM,gBAAgB,WAAW,gBAAgB;KAGnD,MAAM,SAAS,SAAS;AAExB,UAAK,SAAS,wBAAwB;MACpC,aAAa,QAAQ;MACrB,gBAAgB;MACjB,CAAC;AAEF,aAAQ,QAAQ,OAAhB;MACE,KAAK;MACL,KAAK,UAEH;MACF,KAAK;AACH,YAAK,aAAa,uBAAuB,UAAU;AACnD,YAAK,aAAa,gCAAgC,YAAY;AAC9D,YAAK,SAAS,qBAAqB;QACjC,iBAAiB;QACjB,oBAAoB;QACrB,CAAC;AACF,YAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAC3C,cAAO,KAAK,oBACV,UACA,iBACA,OACD;MACH,KAAK,SACH,OAAM,eAAe,gBACnB,OAAO,OAAO,SACd,OAAO,OAAO,WACf;MACH,KAAK,WACH,OAAM,eAAe,UAAU;MACjC,KAAK,SACH,OAAM,eAAe,eAAe;MACtC,QACE,OAAM,eAAe,aACnB,OAAO,QAAQ,SAAS,UAAU,CACnC;;AAIL,WAAM,IAAI,SAAS,YAAY,WAAW,SAAS,MAAM,CAAC;AAC1D,aAAQ,KAAK,IAAI,QAAQ,GAAG,qBAAqB;;YAE5C,OAAO;AACd,SAAK,gBAAgB,MAAe;AACpC,SAAK,UAAU;KACb,MAAM,eAAe;KACrB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;KAChE,CAAC;AAGF,SAAK,uBAAuB,MAAM;aAC1B;AACR,SAAK,KAAK;;KAGd;GAAE,MAAM,KAAK;GAAM,eAAe;GAAM,CACzC;;CAGH,MAAc,oBACZ,UACA,iBACA,QACA;AACA,MAAI,SAAS,UAAU,WAAW,gBAAgB;GAChD,MAAM,SAAS,SAAS;AAQxB,OAAI,QAAQ,WACV,QAAO,KAAK,yBAAyB,UAAU,OAAO,WAAW;AAQnE,OAAI,QAAQ,kBAAkB,OAAO,eAAe,SAAS,EAC3D,QAAO,KAAK,sBAAsB,UAAU,iBAAiB,OAAO;AAatE,QAFE,CAAC,QAAQ,cACR,MAAM,QAAQ,OAAO,WAAW,IAAI,OAAO,WAAW,WAAW,MACnD,SAAS,UAAU,QAAQ,SAAS;IACnD,MAAM,cAAc,yBAClB,SAAS,SAAS,OAAO,QAC1B;AACD,WAAO;KACL,GAAG;KACH,QAAQ;MAAE,GAAI,UAAU,EAAE;MAAG,YAAY;MAAa;KACvD;;;AASL,MAAI,CAAC,SAAS,QAAQ,cAAc,CAAC,SAAS,UAAU,QAAQ,QAC9D,QAAO;EAGT,MAAM,UAAU,SAAS,SAAS,OAAO;EAEzC,MAAM,kBAAkB,SAAS,OAAO,WAAW,KAAK,QAAQ;GAC9D,MAAM,MAA+B,EAAE;AACvC,OAAI,SAAS,OAAO,UAAU;IAC5B,MAAM,SAAS,QAAQ;IACvB,MAAM,aAAa,QAAQ,QAAQ,UAAU;AAG7C,QACE,QAAQ,cAAc,YACtB,OAAO,UAAU,YACjB,UACC,MAAM,OAAO,OAAO,MAAM,OAAO,KAElC,KAAI;AACF,SAAI,cAAc,KAAK,MAAM,MAAM;YAC7B;AAEN,SAAI,cAAc;;QAGpB,KAAI,cAAc;KAEpB;AACF,UAAO;IACP;EAGF,MAAM,EAAE,YAAY,aAAa,GAAG,eAAe,SAAS;AAC5D,SAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAG;IACH,MAAM;IACP;GACF;;;;;;;;;;;;;CAcH,AAAQ,yBACN,UACA,YACA;EAQA,MAAM,WAAW,WAAW,QAAQ,QAAQ,GAAG;EAC/C,MAAM,UAAU,SAAS,SAAS,KAAK,GACnC,IACA,SAAS,SAAS,IAAI,GACpB,IACA;EACN,MAAM,cAAc,KAAK,MAAO,SAAS,SAAS,IAAK,EAAE,GAAG;AAC5D,MAAI,cAAc,4BAChB,OAAM,eAAe,gBACnB,iDAAiD,YAAY,KAAK,4BAA4B,SAC/F;EAGH,MAAM,cAAc,iBAAiB,SAAS;AAC9C,MAAI,YACF,QAAO;GACL,GAAG;GACH,QAAQ;IACN,GAAI,SAAS;IAOb,cAAc,SAAS;IACvB;IACD;GACF;AAGH,SAAO;;CAGT,MAAc,sBACZ,UACA,iBACA,QASC;EACD,MAAM,cAAc,SAAS;AAC7B,SAAO,EACL,QAAQ;GACN,cAAc;GACd,QAAQ;IACN,OAAO,SAAS,QAAQ;IACxB,OAAO,SAAS,QAAQ;IACzB;GACD,aAAa,iBAAiB,SAAS;GAMvC,gBAAgB,MAAM,KAAK,yBACzB,iBACA,aACA,UACA,OACD;GAID,kBAAkB,KAAK,wBACrB,iBACA,YACD;GACF,EACF;;;;;;;;;;;;;CAcH,MAAc,yBACZ,iBACA,aACA,UACA,QACyC;EACzC,MAAM,QAAQ,SAAS,QAAQ;AAC/B,MAAI,CAAC,SAAS,MAAM,WAAW,EAAG,QAAO;EAEzC,MAAM,QAA4B,CAAC,GAAG,MAAM;EAM5C,MAAM,aACJ,SAAS,UAAU,qBAAqB;EAC1C,IAAI,OAAO,KAAK,gBAAgB,MAAM;AACtC,OAAK,IAAI,UAAU,GAAG,QAAQ,QAAQ,UAAU,YAAY,WAAW;AACrE,OAAI,QAAQ,QAAS,OAAM,eAAe,UAAU;GAMpD,MAAM,cAJJ,MAAM,gBAAgB,mBAAmB,yBACvC;IAAE,cAAc;IAAa,aAAa;IAAM,EAChD,KAAK,eAAe,OAAO,CAC5B,EACsB,kBAAkB,EAAE;AAC7C,OAAI,WAAW,WAAW,EAAG;AAC7B,SAAM,KAAK,GAAG,WAAW;AACzB,UAAO,KAAK,gBAAgB,WAAW;;AAEzC,SAAO;;;CAIT,AAAQ,gBAAgB,OAA+C;AACrE,OAAK,MAAM,QAAQ,MACjB,KAAI,KAAK,oBAAoB,KAAM,QAAO,KAAK;;;;;;;;;CAYnD,AAAQ,wBACN,iBACA,aACkB;AAClB,SAAO,OAAO,YAAY,WAAW;AAMnC,WAJE,MAAM,gBAAgB,mBAAmB,yBACvC;IAAE,cAAc;IAAa,aAAa;IAAY,EACtD,KAAK,eAAe,OAAO,CAC5B,EACU,gBAAgB,MAAM,MAAM,EAAE,gBAAgB,WAAW;;;;;;;;;;CAW1E,oBACE,QACA,QACA,SAC2C;AAC3C,SAAO,KAAK,eAAe,aAAa,QAAQ,QAAQ,QAAQ;;;;;;;;CASlE,MAAM,eACJ,iBACA,OACA,QAC+B;AAK/B,SAAO,iBAJU,MAAM,gBAAgB,mBAAmB,aACxD,EAAE,cAAc,OAAO,EACvB,KAAK,eAAe,OAAO,CAC5B,CACgC;;;;;;;;;;CAWnC,AAAQ,uBAAuB,OAAuB;AACpD,MAAI,iBAAiB,SAAS,MAAM,SAAS,aAC3C,OAAM;AAER,MAAI,iBAAiB,YACnB,OAAM;EAER,MAAM,eACJ,SAAS,OAAO,UAAU,YAAY,eAAe,QAChD,MAAkC,YACnC;AACN,QAAM,eAAe,gBACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EACtD,OAAO,iBAAiB,WAAW,eAAe,OACnD;;CAIH,AAAQ,eAAe,QAAsB;AAC3C,SAAO,IAAI,QAAQ,EACjB,mBAAmB;GACjB,yBAAyB,QAAQ,WAAW;GAC5C,0BAA0B,OAAmB;AAC3C,YAAQ,iBAAiB,SAAS,IAAI,EAAE,MAAM,MAAM,CAAC;;GAExD,EACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/sql-warehouse/client.ts"],"sourcesContent":["import type { TelemetryOptions } from \"shared\";\n\nimport {\n AppKitError,\n ConfigurationError,\n ConnectionError,\n ExecutionError,\n ValidationError,\n} from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n ArrowStreamProcessor,\n type RefreshChunkLink,\n} from \"../../stream/arrow-stream-processor\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport {\n Context,\n type sql,\n type WorkspaceClient,\n} from \"../../workspace-client\";\nimport { buildEmptyArrowIPCBase64 } from \"./arrow-schema\";\nimport { executeStatementDefaults } from \"./defaults\";\nimport { WarehousePollBackoff } from \"./warehouse-poll-backoff\";\nimport { WarehouseStatusEmitter } from \"./warehouse-status-emitter\";\n\nconst logger = createLogger(\"connectors:sql-warehouse\");\n\n/**\n * Real column names from a statement's result manifest (positional\n * `column_N` fallback for any blank ones). Databricks encodes ARROW_STREAM\n * schemas positionally (`col_0`, …); these names let consumers relabel an\n * Arrow result to match the JSON path. Returns `undefined` when the manifest\n * carries no columns.\n */\nfunction arrowColumnNames(\n response: sql.StatementResponse,\n): string[] | undefined {\n const cols = response.manifest?.schema?.columns;\n if (!cols || cols.length === 0) return undefined;\n return cols.map((c, i) =>\n c.name && c.name.length > 0 ? c.name : `column_${i}`,\n );\n}\n\n/**\n * Maximum size for inline Arrow IPC attachments (25 MiB decoded — the\n * Databricks Statement Execution API hard cap on INLINE responses).\n *\n * The analytics route streams the decoded attachment straight back on the\n * query response body; this cap bounds the single in-memory copy. Larger\n * results fall through to `disposition: \"EXTERNAL_LINKS\"`, which streams\n * chunk-by-chunk and is not bound by this limit.\n */\nconst MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024;\n\n/**\n * Safety cap on how many additional EXTERNAL_LINKS chunks\n * {@link SQLWarehouseConnector._resolveAllExternalLinks} will follow when the\n * manifest omits `total_chunk_count`. High enough to cover any real result;\n * only bounds a misbehaving warehouse with a cyclic `next_chunk_index`.\n */\nconst MAX_EXTERNAL_CHUNK_FOLLOWS = 10_000;\n\ninterface SQLWarehouseConfig {\n timeout?: number;\n telemetry?: TelemetryOptions;\n}\n\n/**\n * Default ceiling for how long {@link SQLWarehouseConnector.ensureWarehouseRunning}\n * will wait for a warehouse to reach the RUNNING state before giving up.\n *\n * Five minutes covers a cold-start of a classic warehouse on most workspaces;\n * serverless typically reaches RUNNING within ~30s.\n */\nexport const DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Window during which a recent `RUNNING` observation lets subsequent calls\n * to {@link SQLWarehouseConnector.ensureWarehouseRunning} short-circuit\n * without making any SDK calls. Sized to roughly the upper bound of how\n * long Databricks keeps a warehouse \"stickily\" available between requests\n * — past that, we re-verify.\n */\nconst WAREHOUSE_RUNNING_CACHE_TTL_MS = 30_000;\n\n/**\n * A single observation of the warehouse state, emitted by\n * {@link SQLWarehouseConnector.ensureWarehouseRunning} so callers can stream\n * progress to clients (e.g. over SSE).\n *\n * Note: `health.summary` from the SDK is intentionally NOT exposed on this\n * type. It's free-form operator-oriented diagnostic text (cluster IDs,\n * capacity-failure reasons, internal RPC errors) that must not reach end\n * users. The raw value stays in the OTel span attributes for server-side\n * debugging only.\n */\nexport interface WarehouseStatusUpdate {\n /** Current state from the SDK (RUNNING | STARTING | STOPPED | STOPPING | DELETED | DELETING). */\n state: sql.State;\n /** Milliseconds elapsed since `ensureWarehouseRunning` was called. */\n elapsedMs: number;\n /** 1-based attempt counter — useful for tests and telemetry. */\n attempt: number;\n}\n\n/** Options for {@link SQLWarehouseConnector.ensureWarehouseRunning}. */\ninterface EnsureWarehouseRunningOptions {\n /** Invoked every time the warehouse state changes during the wait. */\n onStatus: (update: WarehouseStatusUpdate) => void;\n /** Aborts the wait. The connector treats abort as `ExecutionError.canceled()`. */\n signal?: AbortSignal;\n /** Hard ceiling on the total wait. Defaults to {@link DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS}. */\n timeoutMs?: number;\n /**\n * When `true` (default), a `STOPPED` warehouse is auto-started.\n * When `false`, `STOPPED` surfaces as `ConfigurationError` immediately —\n * useful for cost-controlled deployments that don't want any caller to\n * trigger billable warehouse starts.\n */\n autoStart?: boolean;\n /** @internal Fired once when `warehouses.start` is issued for this poll. */\n onWarehouseStartIssued?: () => void;\n}\n\n/** In-flight warehouse readiness work shared across concurrent callers. */\ninterface WarehouseReadinessInFlight {\n promise: Promise<void>;\n refCount: number;\n sharedController: AbortController;\n subscribers: Set<(update: WarehouseStatusUpdate) => void>;\n /** Last emitted update; replayed to late joiners. */\n lastUpdate: WarehouseStatusUpdate | null;\n /** `true` once this poll has called `warehouses.start`. */\n warehouseStartIssued: boolean;\n}\n\nexport class SQLWarehouseConnector {\n private readonly name = \"sql-warehouse\";\n\n private config: SQLWarehouseConfig;\n\n // Lazy-initialized: only created when Arrow format is used\n private _arrowProcessor: ArrowStreamProcessor | null = null;\n\n /**\n * Per-warehouse cache of the last RUNNING observation timestamp. Used by\n * {@link ensureWarehouseRunning} to short-circuit warm-path callers; see\n * {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}.\n */\n private _recentlyRunning = new Map<string, number>();\n /** Per-warehouse readiness singleflight — one poll loop per cold start. */\n private _readinessInFlight = new Map<string, WarehouseReadinessInFlight>();\n // telemetry\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n queryCount: Counter;\n queryDuration: Histogram;\n };\n\n constructor(config: SQLWarehouseConfig) {\n this.config = config;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n queryCount: this.telemetry.getMeter().createCounter(\"query.count\", {\n description: \"Total number of queries executed\",\n unit: \"1\",\n }),\n queryDuration: this.telemetry\n .getMeter()\n .createHistogram(\"query.duration\", {\n description: \"Duration of queries executed\",\n unit: \"ms\",\n }),\n };\n }\n\n /**\n * Lazily initializes and returns the ArrowStreamProcessor.\n * Only created on first Arrow format query to avoid unnecessary allocation.\n */\n private get arrowProcessor(): ArrowStreamProcessor {\n if (!this._arrowProcessor) {\n this._arrowProcessor = new ArrowStreamProcessor({\n timeout: this.config.timeout || executeStatementDefaults.timeout,\n retries: ArrowStreamProcessor.DEFAULT_RETRIES,\n });\n }\n return this._arrowProcessor;\n }\n\n async executeStatement(\n workspaceClient: WorkspaceClient,\n input: sql.ExecuteStatementRequest,\n signal?: AbortSignal,\n ) {\n const startTime = Date.now();\n let success = false;\n\n // if signal is aborted, throw an error\n if (signal?.aborted) {\n throw ExecutionError.canceled();\n }\n\n return this.telemetry.startActiveSpan(\n \"sql.query\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": input.warehouse_id || \"\",\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n \"db.has_parameters\": !!input.parameters,\n },\n },\n async (span: Span) => {\n let abortHandler: (() => void) | undefined;\n let isAborted = false;\n\n if (signal) {\n abortHandler = () => {\n // abort span if not recording\n if (!span.isRecording()) return;\n isAborted = true;\n span.setAttribute(\"cancelled\", true);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: \"Query cancelled by client\",\n });\n span.end();\n };\n signal.addEventListener(\"abort\", abortHandler, { once: true });\n }\n\n try {\n // validate required fields\n if (!input.statement) {\n throw ValidationError.missingField(\"statement\");\n }\n\n if (!input.warehouse_id) {\n throw ValidationError.missingField(\"warehouse_id\");\n }\n\n const body: sql.ExecuteStatementRequest = {\n statement: input.statement,\n parameters: input.parameters,\n warehouse_id: input.warehouse_id,\n catalog: input.catalog,\n schema: input.schema,\n wait_timeout:\n input.wait_timeout || executeStatementDefaults.wait_timeout,\n disposition:\n input.disposition || executeStatementDefaults.disposition,\n format: input.format || executeStatementDefaults.format,\n byte_limit: input.byte_limit,\n row_limit: input.row_limit,\n on_wait_timeout:\n input.on_wait_timeout || executeStatementDefaults.on_wait_timeout,\n };\n\n span.addEvent(\"statement.submitting\", {\n \"db.warehouse_id\": input.warehouse_id,\n });\n\n const response =\n await workspaceClient.statementExecution.executeStatement(\n body,\n this._createContext(signal),\n );\n\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n const status = response.status;\n const statementId = response.statement_id as string;\n\n span.setAttribute(\"db.statement_id\", statementId);\n span.addEvent(\"statement.submitted\", {\n \"db.statement_id\": response.statement_id,\n \"db.status\": status?.state,\n });\n\n let result:\n | sql.StatementResponse\n | { result: { statement_id: string; status: sql.StatementStatus } };\n\n switch (status?.state) {\n case \"RUNNING\":\n case \"PENDING\":\n span.addEvent(\"statement.polling_started\", {\n \"db.status\": response.status?.state,\n });\n result = await this._pollForStatementResult(\n workspaceClient,\n statementId,\n this.config.timeout,\n signal,\n );\n break;\n case \"SUCCEEDED\":\n result = await this._transformDataArray(\n response,\n workspaceClient,\n signal,\n );\n break;\n case \"FAILED\":\n throw ExecutionError.statementFailed(\n status.error?.message,\n status.error?.error_code,\n );\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n const resultData = result.result as any;\n const rowCount =\n resultData?.data?.length ?? resultData?.data_array?.length ?? 0;\n\n if (rowCount > 0) {\n span.setAttribute(\"db.result.row_count\", rowCount);\n }\n\n const duration = Date.now() - startTime;\n logger.event()?.setContext(\"sql-warehouse\", {\n warehouse_id: input.warehouse_id,\n rows_returned: rowCount,\n query_duration_ms: duration,\n });\n\n success = true;\n // only set success status if not aborted\n if (!isAborted) {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n return result;\n } catch (error) {\n // only record error if not already handled by abort\n if (!isAborted) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n logger.error(\n \"Statement execution failed: %s\",\n error instanceof Error ? error.message : String(error),\n );\n }\n\n this._rethrowStatementError(error);\n } finally {\n // remove abort handler\n if (abortHandler && signal) {\n signal.removeEventListener(\"abort\", abortHandler);\n }\n\n const duration = Date.now() - startTime;\n\n // end span if not already ended by abort handler\n if (!isAborted) {\n span.end();\n }\n\n const attributes = {\n \"db.warehouse_id\": input.warehouse_id,\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n success: success.toString(),\n };\n\n this.telemetryMetrics.queryCount.add(1, attributes);\n this.telemetryMetrics.queryDuration.record(duration, attributes);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait until the SQL warehouse is in the `RUNNING` state, auto-starting it\n * if currently `STOPPED`. Emits a {@link WarehouseStatusUpdate} whenever\n * the observed state changes, so callers can surface progress (e.g. over\n * SSE) instead of letting the UI freeze on a cold warehouse. Equal\n * successive observations are de-duplicated.\n *\n * Fast path: if this connector recently observed the warehouse RUNNING\n * (within {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}), the call returns\n * immediately without any SDK round-trip or status emission. This keeps\n * cache-hit analytics requests off the Databricks control plane.\n *\n * Concurrent callers for the same `warehouseId` share a single in-flight\n * poll loop (singleflight): only the owner issues `get`/`start`; joiners\n * receive broadcast `onStatus` updates and share the result promise.\n * When every waiter disconnects before `warehouses.start`, the shared poll\n * is aborted on the next microtask if still unclaimed (kills true orphans\n * without a fixed grace window). After `start` is issued, the poll runs to\n * completion even with no waiters.\n *\n * Behaviour by initial state:\n * - `RUNNING`: emits one update, caches the observation, returns.\n * - `STOPPED`: emits a synthetic `STARTING` update, calls\n * `workspaceClient.warehouses.start`, then polls until `RUNNING`.\n * When `autoStart: false`, throws `ConfigurationError` instead.\n * - `STARTING` / `STOPPING`: polls until `RUNNING`.\n * - `DELETED` / `DELETING`: throws `ConfigurationError.resourceNotFound`.\n *\n * Aborts and timeouts surface as `ExecutionError`. The poll loop uses\n * exponential backoff with ±15% jitter (1s → 30s cap) and runs inside a\n * `sql.warehouseReady` telemetry span.\n */\n async ensureWarehouseRunning(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: EnsureWarehouseRunningOptions,\n ): Promise<void> {\n const {\n onStatus,\n signal,\n timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n autoStart = true,\n } = opts;\n\n if (signal?.aborted) throw ExecutionError.canceled();\n if (!warehouseId) throw ValidationError.missingField(\"warehouse_id\");\n if (this._isRecentlyRunning(warehouseId)) return;\n\n const existing = this._readinessInFlight.get(warehouseId);\n if (existing && !existing.sharedController.signal.aborted) {\n return this._waitOnReadiness(existing, onStatus, signal, true);\n }\n\n const entry = this._spawnReadinessInFlight(workspaceClient, warehouseId, {\n timeoutMs,\n autoStart,\n });\n this._readinessInFlight.set(warehouseId, entry);\n return this._waitOnReadiness(entry, onStatus, signal, false);\n }\n\n private _waitOnReadiness(\n entry: WarehouseReadinessInFlight,\n onStatus: (update: WarehouseStatusUpdate) => void,\n signal: AbortSignal | undefined,\n join: boolean,\n ): Promise<void> {\n if (signal?.aborted) throw ExecutionError.canceled();\n if (join) entry.refCount++;\n entry.subscribers.add(onStatus);\n if (entry.lastUpdate) onStatus(entry.lastUpdate);\n return this._joinWarehouseReadiness(entry, signal);\n }\n\n private _spawnReadinessInFlight(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: { timeoutMs: number; autoStart: boolean },\n ): WarehouseReadinessInFlight {\n const sharedController = new AbortController();\n const entry: WarehouseReadinessInFlight = {\n refCount: 1,\n sharedController,\n subscribers: new Set(),\n lastUpdate: null,\n warehouseStartIssued: false,\n promise: Promise.resolve(),\n };\n\n entry.promise = this.telemetry\n .startActiveSpan(\n \"sql.warehouseReady\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": warehouseId,\n \"db.warehouse.startup_timeout_ms\": opts.timeoutMs,\n },\n },\n async (span: Span) => {\n try {\n await this._pollUntilWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n onStatus: (update) => this._broadcastReadiness(entry, update),\n signal: sharedController.signal,\n timeoutMs: opts.timeoutMs,\n autoStart: opts.autoStart,\n onWarehouseStartIssued: () => {\n entry.warehouseStartIssued = true;\n },\n },\n span,\n );\n span.setStatus({ code: SpanStatusCode.OK });\n } catch (error) {\n this._throwSanitizedReadinessError(error, warehouseId, span);\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n )\n .finally(() => {\n if (this._readinessInFlight.get(warehouseId) === entry) {\n this._readinessInFlight.delete(warehouseId);\n }\n });\n\n entry.promise.catch(() => {});\n return entry;\n }\n\n private _broadcastReadiness(\n entry: WarehouseReadinessInFlight,\n update: WarehouseStatusUpdate,\n ): void {\n entry.lastUpdate = update;\n for (const subscriber of entry.subscribers) {\n try {\n subscriber(update);\n } catch {\n // One consumer must not block updates to the rest.\n }\n }\n }\n\n /** Ref-counted await; caller abort rejects locally. */\n private _joinWarehouseReadiness(\n entry: WarehouseReadinessInFlight,\n callerSignal?: AbortSignal,\n ): Promise<void> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<void>((resolve, reject) => {\n let done = false;\n const finish = (outcome: \"ok\" | \"abort\" | \"err\", error?: unknown) => {\n if (done) return;\n done = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n if (outcome === \"ok\") resolve();\n else if (outcome === \"abort\") reject(ExecutionError.canceled());\n else reject(error);\n };\n const onAbort = () => {\n this._releaseReadinessWaiter(entry);\n finish(\"abort\");\n };\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n entry.promise.then(\n () => finish(\"ok\"),\n (error) => finish(\"err\", error),\n );\n });\n }\n\n private _releaseReadinessWaiter(entry: WarehouseReadinessInFlight): void {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount > 0 || entry.sharedController.signal.aborted) return;\n\n if (entry.warehouseStartIssued) {\n // Start already issued — finish warming for the process.\n return;\n }\n\n // Defer abort to the next microtask so a synchronous StrictMode\n // remount can rejoin before the shared poll is cancelled.\n queueMicrotask(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\"all warehouse readiness waiters aborted\");\n }\n });\n }\n\n private async _pollUntilWarehouseRunning(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: EnsureWarehouseRunningOptions,\n span: Span,\n ): Promise<void> {\n const {\n onStatus,\n signal,\n timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n autoStart = true,\n onWarehouseStartIssued,\n } = opts;\n const startTime = Date.now();\n const emitter = new WarehouseStatusEmitter(span, startTime, onStatus);\n let didStart = false;\n const backoff = new WarehousePollBackoff();\n\n while (true) {\n if (signal?.aborted) throw ExecutionError.canceled();\n if (Date.now() - startTime > timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n\n const info = await workspaceClient.warehouses.get(\n { id: warehouseId },\n this._createContext(signal),\n );\n const state = info?.state;\n const summary = info?.health?.summary;\n\n switch (state) {\n case \"RUNNING\":\n emitter.emit(state, summary);\n this._recentlyRunning.set(warehouseId, Date.now());\n span.setAttribute(\"db.warehouse.attempts\", emitter.attempt);\n return;\n case \"DELETED\":\n case \"DELETING\":\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n `The configured SQL warehouse is ${state}. Update DATABRICKS_WAREHOUSE_ID to point at an active warehouse.`,\n );\n case \"STOPPED\":\n if (!autoStart) {\n throw new ConfigurationError(\n \"The configured SQL warehouse is STOPPED and analytics auto-start is disabled. Start the warehouse manually or set analytics.autoStartWarehouse=true.\",\n );\n }\n if (!didStart) {\n emitter.emit(\"STARTING\", summary);\n onWarehouseStartIssued?.();\n await workspaceClient.warehouses.start(\n { id: warehouseId },\n this._createContext(signal),\n );\n didStart = true;\n } else {\n emitter.emit(state, summary);\n }\n break;\n case \"STARTING\":\n case \"STOPPING\":\n emitter.emit(state, summary);\n break;\n default:\n throw ExecutionError.unknownState(String(state ?? \"unknown\"));\n }\n\n const sleepMs = backoff.next();\n if (Date.now() + sleepMs - startTime >= timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n await this._sleepRespectingAbort(sleepMs, signal);\n }\n }\n\n /**\n * `true` if this connector observed `warehouseId` in the RUNNING state\n * within the recent-cache TTL.\n */\n private _isRecentlyRunning(warehouseId: string): boolean {\n const observedAt = this._recentlyRunning.get(warehouseId);\n return (\n observedAt !== undefined &&\n Date.now() - observedAt < WAREHOUSE_RUNNING_CACHE_TTL_MS\n );\n }\n\n /**\n * Final landing for any error thrown by the readiness poll loop. Records\n * the original on the span and (at debug level) on the logger, then\n * rethrows either the structured AppKitError unchanged or a curated\n * ExecutionError — never the raw SDK message, which can contain operator\n * internals.\n *\n * The `logger.debug` covers environments where OTel isn't configured\n * (common in dev) so the raw error isn't lost just because no span\n * exporter is hooked up.\n */\n private _throwSanitizedReadinessError(\n error: unknown,\n warehouseId: string,\n span: Span,\n ): never {\n if (error instanceof AppKitError) {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.code });\n throw error;\n }\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n logger.debug(\n \"Warehouse readiness check raw error for %s: %O\",\n warehouseId,\n error,\n );\n const wrapped = ExecutionError.statementFailed(\n \"Warehouse readiness check failed\",\n );\n span.setStatus({ code: SpanStatusCode.ERROR, message: wrapped.code });\n throw wrapped;\n }\n\n /**\n * Sleep for `ms` milliseconds, but resolve early (and reject with\n * `ExecutionError.canceled()`) if `signal` aborts mid-sleep. Used by the\n * warehouse readiness loop so that client disconnects don't keep the loop\n * polling for the full interval.\n */\n private _sleepRespectingAbort(\n ms: number,\n signal?: AbortSignal,\n ): Promise<void> {\n if (signal?.aborted) return Promise.reject(ExecutionError.canceled());\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(ExecutionError.canceled());\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n private async _pollForStatementResult(\n workspaceClient: WorkspaceClient,\n statementId: string,\n timeout = executeStatementDefaults.timeout,\n signal?: AbortSignal,\n ) {\n return this.telemetry.startActiveSpan(\n \"sql.poll\",\n {\n attributes: {\n \"db.statement_id\": statementId,\n \"db.polling.timeout\": timeout,\n },\n },\n async (span: Span) => {\n try {\n const startTime = Date.now();\n let delay = 1000;\n const maxDelayBetweenPolls = 5000; // max 5 seconds between polls\n let pollCount = 0;\n\n while (true) {\n pollCount++;\n span.setAttribute(\"db.polling.current_attempt\", pollCount);\n\n // check if timeout exceeded\n const elapsedTime = Date.now() - startTime;\n if (elapsedTime > timeout) {\n const error = ExecutionError.statementFailed(\n `Polling timeout exceeded after ${timeout}ms (elapsed: ${elapsedTime}ms)`,\n );\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n if (signal?.aborted) {\n const error = ExecutionError.canceled();\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n span.addEvent(\"polling.attempt\", {\n \"poll.attempt\": pollCount,\n \"poll.delay_ms\": delay,\n \"poll.elapsed_ms\": elapsedTime,\n });\n\n const response =\n await workspaceClient.statementExecution.getStatement(\n {\n statement_id: statementId,\n },\n this._createContext(signal),\n );\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n\n const status = response.status;\n\n span.addEvent(\"polling.status_check\", {\n \"db.status\": status?.state,\n \"poll.attempt\": pollCount,\n });\n\n switch (status?.state) {\n case \"PENDING\":\n case \"RUNNING\":\n // continue polling\n break;\n case \"SUCCEEDED\":\n span.setAttribute(\"db.polling.attempts\", pollCount);\n span.setAttribute(\"db.polling.total_duration_ms\", elapsedTime);\n span.addEvent(\"polling.completed\", {\n \"poll.attempts\": pollCount,\n \"poll.duration_ms\": elapsedTime,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n return this._transformDataArray(\n response,\n workspaceClient,\n signal,\n );\n case \"FAILED\":\n throw ExecutionError.statementFailed(\n status.error?.message,\n status.error?.error_code,\n );\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n // continue polling after delay\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * 2, maxDelayBetweenPolls);\n }\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n // error logging is handled by executeStatement's catch block (gated on isAborted)\n this._rethrowStatementError(error);\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n private async _transformDataArray(\n response: sql.StatementResponse,\n workspaceClient: WorkspaceClient,\n signal?: AbortSignal,\n ) {\n if (response.manifest?.format === \"ARROW_STREAM\") {\n const result = response.result as\n | (sql.ResultData & { attachment?: string })\n | undefined;\n\n // Inline Arrow: pass the base64 IPC attachment through unmodified so\n // the analytics route can stream it to the client, where the existing\n // ArrowClient infrastructure decodes it into a Table. Validate size\n // here to fail fast on runaway payloads.\n if (result?.attachment) {\n return this._validateArrowAttachment(response, result.attachment);\n }\n\n // External links: data fetched separately via statement_id. An empty\n // array is a zero-row result (some warehouses emit `external_links: []`\n // rather than omitting it) — it must NOT go down the streaming path\n // (`streamChunks([])` rejects), so fall through to synthesize an empty\n // Arrow table below.\n if (result?.external_links && result.external_links.length > 0) {\n return this.updateWithArrowStatus(response, workspaceClient, signal);\n }\n\n // Empty result with a known schema: synthesize a zero-row Arrow IPC\n // attachment so the client always receives an Arrow Table for\n // ARROW_STREAM, regardless of whether the warehouse returned data.\n // Note: an empty array (`data_array: []`) is truthy, so length-check\n // explicitly — otherwise zero-row responses fall through to the JSON\n // row transform below and return `[]` JSON rows instead of an Arrow\n // table.\n const hasNoRows =\n !result?.data_array ||\n (Array.isArray(result.data_array) && result.data_array.length === 0);\n if (hasNoRows && response.manifest?.schema?.columns) {\n const synthesized = buildEmptyArrowIPCBase64(\n response.manifest.schema.columns,\n );\n return {\n ...response,\n result: { ...(result ?? {}), attachment: synthesized },\n };\n }\n\n // Inline data_array under ARROW_STREAM (rare): fall through to the\n // row transform below. The hook will receive `type: \"result\"` rows;\n // callers asking for ARROW_STREAM should not hit this path with\n // current Databricks warehouses.\n }\n\n if (!response.result?.data_array || !response.manifest?.schema?.columns) {\n return response;\n }\n\n const columns = response.manifest.schema.columns;\n\n const transformedData = response.result.data_array.map((row) => {\n const obj: Record<string, unknown> = {};\n row.forEach((value, index) => {\n const column = columns[index];\n const columnName = column?.name || `column_${index}`;\n\n // attempt to parse JSON strings for string columns\n if (\n column?.type_name === \"STRING\" &&\n typeof value === \"string\" &&\n value &&\n (value[0] === \"{\" || value[0] === \"[\")\n ) {\n try {\n obj[columnName] = JSON.parse(value);\n } catch {\n // if parsing fails, keep as string\n obj[columnName] = value;\n }\n } else {\n obj[columnName] = value;\n }\n });\n return obj;\n });\n\n // remove data_array\n const { data_array: _data_array, ...restResult } = response.result;\n return {\n ...response,\n result: {\n ...restResult,\n data: transformedData,\n },\n };\n }\n\n /**\n * Validate a base64 Arrow IPC attachment (INLINE ARROW_STREAM) and attach\n * the real column names from the result manifest.\n *\n * Databricks warehouses encode the Arrow schema positionally (`col_0`, …);\n * the real, aliased names live only in `manifest.schema.columns`. Rather\n * than decode/re-encode the payload server-side, we carry the names on the\n * result (`columnNames`) so the route can hand them to the client in a\n * response header and the client relabels the decoded Table — the one\n * mechanism used for both INLINE and EXTERNAL_LINKS.\n */\n private _validateArrowAttachment(\n response: sql.StatementResponse,\n attachment: string,\n ) {\n // Cap the size to protect against unbounded inline payloads from\n // misbehaving warehouses. `MAX_INLINE_ATTACHMENT_BYTES` tracks the API's\n // ~25 MiB inline hard cap and bounds memory if a server returns a runaway\n // response.\n //\n // Strip whitespace (rare but legal in base64) and account for trailing\n // `=` padding so the byte count is exact rather than an upper bound.\n const stripped = attachment.replace(/\\s+/g, \"\");\n const padding = stripped.endsWith(\"==\")\n ? 2\n : stripped.endsWith(\"=\")\n ? 1\n : 0;\n const decodedSize = Math.floor((stripped.length * 3) / 4) - padding;\n if (decodedSize > MAX_INLINE_ATTACHMENT_BYTES) {\n throw ExecutionError.statementFailed(\n `Inline Arrow attachment exceeds maximum size (${decodedSize} > ${MAX_INLINE_ATTACHMENT_BYTES} bytes)`,\n );\n }\n\n const columnNames = arrowColumnNames(response);\n if (columnNames) {\n return {\n ...response,\n result: {\n ...(response.result as sql.ResultData & {\n attachment?: string;\n columnNames?: string[];\n }),\n // `statement_id` is a top-level field, not on `ResultData` — carry it\n // onto the result (as the EXTERNAL_LINKS path does) so the route can\n // advertise it in `X-Appkit-Arrow-Columns-Ref` for wide inline schemas.\n statement_id: response.statement_id,\n columnNames,\n },\n };\n }\n\n return response;\n }\n\n private async updateWithArrowStatus(\n response: sql.StatementResponse,\n workspaceClient: WorkspaceClient,\n signal?: AbortSignal,\n ): Promise<{\n result: {\n statement_id: string;\n status: sql.StatementStatus;\n columnNames?: string[];\n external_links?: sql.ExternalLink[];\n refreshChunkLink?: RefreshChunkLink;\n };\n }> {\n const statementId = response.statement_id as string;\n return {\n result: {\n statement_id: statementId,\n status: {\n state: response.status?.state,\n error: response.status?.error,\n } as sql.StatementStatus,\n columnNames: arrowColumnNames(response),\n // Resolve the pre-signed links for EVERY chunk in the caller's own\n // execution context. Streaming these directly (see\n // {@link streamExternalLinks}) avoids a second `getStatement` under\n // the ambient service-principal context — which would fail for\n // `.obo.sql` (user-owned) statements.\n external_links: await this._resolveAllExternalLinks(\n workspaceClient,\n statementId,\n response,\n signal,\n ),\n // Bound to this caller's identity so the streamer can re-mint an\n // expired chunk link mid-download (links live <= 15 min; a large/slow\n // result can outlast the earliest ones).\n refreshChunkLink: this._makeChunkLinkRefresher(\n workspaceClient,\n statementId,\n ),\n },\n };\n }\n\n /**\n * Resolve pre-signed links for EVERY chunk of an EXTERNAL_LINKS result.\n *\n * The execute/getStatement response carries only the first chunk's links\n * (each link, except the last, exposes `next_chunk_index`); the remaining\n * chunks are fetched with `getStatementResultChunkN`. Runs in the caller's\n * identity context (user creds for `.obo.sql`), so there is no cross-identity\n * fetch. Only the tiny link metadata is resolved eagerly — the bytes still\n * stream one chunk at a time downstream. Without this a multi-chunk result\n * would be silently truncated to its first chunk.\n */\n private async _resolveAllExternalLinks(\n workspaceClient: WorkspaceClient,\n statementId: string,\n response: sql.StatementResponse,\n signal?: AbortSignal,\n ): Promise<sql.ExternalLink[] | undefined> {\n const first = response.result?.external_links;\n if (!first || first.length === 0) return first;\n\n const links: sql.ExternalLink[] = [...first];\n // Bound the follow loop so a warehouse returning a cyclic/never-ending\n // `next_chunk_index` can't spin forever. The manifest's chunk count is the\n // natural bound; fall back to a generous safety cap if it's absent (real\n // results still terminate earlier when `next_chunk_index` becomes null) so\n // a missing count doesn't silently truncate a genuine multi-chunk result.\n const maxFetches =\n response.manifest?.total_chunk_count ?? MAX_EXTERNAL_CHUNK_FOLLOWS;\n let next = this._nextChunkIndex(first);\n for (let fetches = 0; next != null && fetches < maxFetches; fetches++) {\n if (signal?.aborted) throw ExecutionError.canceled();\n const chunk =\n await workspaceClient.statementExecution.getStatementResultChunkN(\n { statement_id: statementId, chunk_index: next },\n this._createContext(signal),\n );\n const chunkLinks = chunk.external_links ?? [];\n if (chunkLinks.length === 0) break;\n links.push(...chunkLinks);\n next = this._nextChunkIndex(chunkLinks);\n }\n return links;\n }\n\n /** The `next_chunk_index` advertised by a chunk's links, if any. */\n private _nextChunkIndex(links: sql.ExternalLink[]): number | undefined {\n for (const link of links) {\n if (link.next_chunk_index != null) return link.next_chunk_index;\n }\n return undefined;\n }\n\n /**\n * A closure that re-mints a single chunk's pre-signed link via\n * `getStatementResultChunkN`, bound to the caller's workspace client +\n * statement id. Created here (in the caller's identity context) so the\n * streamer — which runs outside that context — can refresh an expired link\n * for `.obo.sql` statements without a cross-identity `getStatement`.\n */\n private _makeChunkLinkRefresher(\n workspaceClient: WorkspaceClient,\n statementId: string,\n ): RefreshChunkLink {\n return async (chunkIndex, signal) => {\n const chunk =\n await workspaceClient.statementExecution.getStatementResultChunkN(\n { statement_id: statementId, chunk_index: chunkIndex },\n this._createContext(signal),\n );\n return chunk.external_links?.find((l) => l.chunk_index === chunkIndex);\n };\n }\n\n /**\n * Stream already-resolved EXTERNAL_LINKS chunks as raw Arrow IPC bytes, one\n * chunk in memory at a time. Takes the `external_links` the caller already\n * has from `executeStatement` (see {@link updateWithArrowStatus}), so there\n * is no extra `getStatement` round-trip and no execution-context mismatch —\n * the pre-signed URLs need no auth to download.\n */\n streamExternalLinks(\n chunks: sql.ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n return this.arrowProcessor.streamChunks(chunks, signal, refresh);\n }\n\n /**\n * Fetch just the real column names for a completed statement from its result\n * manifest — no result data. Backs the `/columns/:statementId` fallback the\n * client uses when a very wide schema's names exceed the response-header\n * size limit. Stateless: re-derives from the warehouse, no server cache.\n */\n async getColumnNames(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<string[] | undefined> {\n const response = await workspaceClient.statementExecution.getStatement(\n { statement_id: jobId },\n this._createContext(signal),\n );\n return arrowColumnNames(response);\n }\n\n /**\n * Normalize an error caught during statement execution/polling and rethrow.\n * Abort and already-structured `AppKitError`s pass through untouched;\n * anything else is wrapped as an `ExecutionError`, preserving the SDK's\n * structured `ApiError.errorCode` (e.g. \"INVALID_PARAMETER_VALUE\",\n * \"BAD_REQUEST\") so callers can branch on a stable identifier rather than\n * substring-matching the message.\n */\n private _rethrowStatementError(error: unknown): never {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw error;\n }\n if (error instanceof AppKitError) {\n throw error;\n }\n const sdkErrorCode =\n error && typeof error === \"object\" && \"errorCode\" in error\n ? (error as { errorCode?: unknown }).errorCode\n : undefined;\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n typeof sdkErrorCode === \"string\" ? sdkErrorCode : undefined,\n );\n }\n\n // create context for cancellation token\n private _createContext(signal?: AbortSignal) {\n return new Context({\n cancellationToken: {\n isCancellationRequested: signal?.aborted ?? false,\n onCancellationRequested: (cb: () => void) => {\n signal?.addEventListener(\"abort\", cb, { once: true });\n },\n },\n });\n }\n}\n"],"mappings":"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"}
|