@databricks/appkit 0.51.0 → 0.52.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.
Files changed (94) hide show
  1. package/CLAUDE.md +3 -0
  2. package/dist/agents/databricks.d.ts +2 -3
  3. package/dist/agents/databricks.d.ts.map +1 -1
  4. package/dist/agents/databricks.js +5 -4
  5. package/dist/agents/databricks.js.map +1 -1
  6. package/dist/agents/supervisor-api.d.ts.map +1 -1
  7. package/dist/agents/supervisor-api.js +3 -1
  8. package/dist/agents/supervisor-api.js.map +1 -1
  9. package/dist/appkit/package.js +1 -1
  10. package/dist/cache/index.d.ts.map +1 -1
  11. package/dist/cache/index.js +4 -2
  12. package/dist/cache/index.js.map +1 -1
  13. package/dist/connectors/files/client.js +2 -1
  14. package/dist/connectors/files/client.js.map +1 -1
  15. package/dist/connectors/genie/client.d.ts +2 -4
  16. package/dist/connectors/genie/client.js +2 -3
  17. package/dist/connectors/genie/client.js.map +1 -1
  18. package/dist/connectors/jobs/client.d.ts +2 -2
  19. package/dist/connectors/jobs/client.js +2 -1
  20. package/dist/connectors/jobs/client.js.map +1 -1
  21. package/dist/connectors/serving/client.d.ts +1 -1
  22. package/dist/connectors/serving/client.js +2 -1
  23. package/dist/connectors/serving/client.js.map +1 -1
  24. package/dist/connectors/sql-warehouse/client.js +2 -1
  25. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  26. package/dist/connectors/sql-warehouse/defaults.js.map +1 -1
  27. package/dist/connectors/sql-warehouse/warehouse-status-emitter.js.map +1 -1
  28. package/dist/connectors/vector-search/client.js.map +1 -1
  29. package/dist/context/client-options.js.map +1 -1
  30. package/dist/context/execution-context.d.ts +1 -1
  31. package/dist/context/service-context.d.ts +2 -1
  32. package/dist/context/service-context.d.ts.map +1 -1
  33. package/dist/context/service-context.js +8 -5
  34. package/dist/context/service-context.js.map +1 -1
  35. package/dist/core/appkit.d.ts +2 -1
  36. package/dist/core/appkit.d.ts.map +1 -1
  37. package/dist/core/appkit.js.map +1 -1
  38. package/dist/index.d.ts +6 -1
  39. package/dist/index.js +4 -1
  40. package/dist/internal-telemetry/reporter.js.map +1 -1
  41. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  42. package/dist/plugins/analytics/analytics.js.map +1 -1
  43. package/dist/plugins/analytics/query.js.map +1 -1
  44. package/dist/plugins/analytics/result-delivery.js.map +1 -1
  45. package/dist/plugins/files/plugin.js +4 -3
  46. package/dist/plugins/files/plugin.js.map +1 -1
  47. package/dist/plugins/files/types.d.ts +2 -1
  48. package/dist/plugins/files/types.d.ts.map +1 -1
  49. package/dist/plugins/jobs/plugin.js.map +1 -1
  50. package/dist/plugins/jobs/types.d.ts +2 -1
  51. package/dist/plugins/jobs/types.d.ts.map +1 -1
  52. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  53. package/dist/plugins/lakebase/lakebase.js +5 -4
  54. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  55. package/dist/stream/arrow-stream-processor.js.map +1 -1
  56. package/dist/type-generator/index.js +3 -2
  57. package/dist/type-generator/index.js.map +1 -1
  58. package/dist/type-generator/mv-registry/describe.js.map +1 -1
  59. package/dist/type-generator/query-registry.js +3 -2
  60. package/dist/type-generator/query-registry.js.map +1 -1
  61. package/dist/type-generator/serving/fetcher.js +2 -1
  62. package/dist/type-generator/serving/fetcher.js.map +1 -1
  63. package/dist/type-generator/serving/generator.js +3 -2
  64. package/dist/type-generator/serving/generator.js.map +1 -1
  65. package/dist/type-generator/statement-result.js.map +1 -1
  66. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  67. package/dist/type-generator/vite-plugin.js +3 -2
  68. package/dist/type-generator/vite-plugin.js.map +1 -1
  69. package/dist/type-generator/warehouse-status.js.map +1 -1
  70. package/dist/workspace-client/client.js +57 -0
  71. package/dist/workspace-client/client.js.map +1 -0
  72. package/dist/workspace-client/errors.d.ts +2 -0
  73. package/dist/workspace-client/errors.js +3 -0
  74. package/dist/workspace-client/factory.d.ts +19 -0
  75. package/dist/workspace-client/factory.d.ts.map +1 -0
  76. package/dist/workspace-client/factory.js +24 -0
  77. package/dist/workspace-client/factory.js.map +1 -0
  78. package/dist/workspace-client/index.d.ts +4 -0
  79. package/dist/workspace-client/index.js +5 -0
  80. package/dist/workspace-client/legacy.d.ts +29 -0
  81. package/dist/workspace-client/legacy.d.ts.map +1 -0
  82. package/dist/workspace-client/legacy.js +23 -0
  83. package/dist/workspace-client/legacy.js.map +1 -0
  84. package/dist/workspace-client/types.d.ts +49 -0
  85. package/dist/workspace-client/types.d.ts.map +1 -0
  86. package/docs/api/appkit/Class.DatabricksAdapter.md +2 -3
  87. package/docs/api/appkit/Function.createApp.md +9 -9
  88. package/docs/api/appkit/Function.createWorkspaceClient.md +28 -0
  89. package/docs/api/appkit/Interface.WorkspaceClient.md +119 -0
  90. package/docs/api/appkit/Interface.WorkspaceClientOptions.md +47 -0
  91. package/docs/api/appkit.md +3 -0
  92. package/llms.txt +3 -0
  93. package/package.json +1 -1
  94. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { ApiError, WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { getClientOptions } from \"../context/client-options\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Reference-counted in-flight cache execution entry.\n *\n * `sharedController` decouples the cached `fn()` from any single caller's\n * abort signal. Callers join an in-flight entry by incrementing `refCount`;\n * when a caller aborts, refCount is decremented. The shared controller is\n * aborted only when refCount drops to 0 — i.e. all callers have abandoned\n * the request. This prevents one caller's cancellation (e.g. React\n * StrictMode unmount) from poisoning the in-flight result for other still-\n * connected awaiters.\n */\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n refCount: number;\n sharedController: AbortController;\n abortTimer?: ReturnType<typeof setTimeout>;\n}\n\nfunction createAbortError(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason;\n return new DOMException(\"The operation was aborted.\", \"AbortError\");\n}\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private static readonly ABORT_GRACE_PERIOD_MS = 100;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, InFlightEntry<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({}, getClientOptions());\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result.\n *\n * Multiple concurrent callers with the same `cacheKey` are deduplicated\n * onto a single in-flight execution. Each caller may pass its own\n * `callerSignal`; the underlying `fn()` is run with a shared, internally\n * managed `AbortSignal` that aborts only when *all* callers have\n * abandoned the request (reference counted). This decouples a single\n * caller's cancellation (e.g. React StrictMode unmount) from the shared\n * result, so other still-connected callers receive the cached value\n * normally.\n *\n * @param key - Cache key parts\n * @param fn - Function to execute. Receives the cache-owned shared signal;\n * pass it through to the underlying I/O so the work is cancelled when\n * no caller is left waiting.\n * @param userKey - User key for cache namespacing\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: (sharedSignal?: AbortSignal) => Promise<T>,\n userKey: string,\n options?: { ttl?: number; callerSignal?: AbortSignal },\n ): Promise<T> {\n if (!this.config.enabled) return fn(options?.callerSignal);\n\n const callerSignal = options?.callerSignal;\n if (callerSignal?.aborted) {\n throw createAbortError(callerSignal);\n }\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n const cached = await this.getValid<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value;\n }\n\n // check if the value is being processed by another request — join\n // the existing in-flight entry under reference counting so this\n // caller's abort doesn't poison the shared result.\n const existing = this.inFlightRequests.get(cacheKey) as\n | InFlightEntry<T>\n | undefined;\n if (existing && !existing.sharedController.signal.aborted) {\n existing.refCount++;\n // Cancel any pending abort timer — a new caller has joined\n if (existing.abortTimer) {\n clearTimeout(existing.abortTimer);\n existing.abortTimer = undefined;\n }\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n return await this._waitWithRefCount(existing, callerSignal);\n }\n\n // cache miss - execute function under a shared abort controller\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const sharedController = new AbortController();\n const entry: InFlightEntry<T> = {\n promise: undefined as unknown as Promise<T>,\n refCount: 1,\n sharedController,\n };\n\n entry.promise = fn(sharedController.signal)\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // If the shared controller aborted, all callers have already\n // abandoned the request (or are about to via their own signals)\n // — propagate the original error without wrapping. No live\n // awaiter will observe this rejection.\n if (sharedController.signal.aborted) {\n throw error;\n }\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n if (this.inFlightRequests.get(cacheKey) === entry) {\n this.inFlightRequests.delete(cacheKey);\n }\n });\n\n // Suppress unhandled rejection warnings when every caller bailed\n // before fn() resolved (their own promises rejected via\n // waitWithRefCount; the underlying entry.promise has no awaiter).\n entry.promise.catch(() => {});\n\n this.inFlightRequests.set(cacheKey, entry as InFlightEntry<unknown>);\n\n const result = await this._waitWithRefCount(entry, callerSignal);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait on an in-flight entry, racing the underlying promise against the\n * caller's abort signal. When the caller aborts, the entry's refCount is\n * decremented; if it hits zero the shared controller is aborted so the\n * underlying `fn()` can stop. Other callers continue to await the same\n * entry and receive the result when it arrives.\n */\n private _waitWithRefCount<T>(\n entry: InFlightEntry<T>,\n callerSignal?: AbortSignal,\n ): Promise<T> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const release = () => {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n // Grace period: delay abort so a StrictMode remount can join\n // the in-flight entry before the shared execution is cancelled.\n entry.abortTimer = setTimeout(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\n callerSignal.reason ?? \"all cache callers aborted\",\n );\n }\n }, CacheManager.ABORT_GRACE_PERIOD_MS);\n }\n };\n\n const onAbort = () => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n release();\n reject(createAbortError(callerSignal));\n };\n\n if (callerSignal.aborted) {\n onAbort();\n return;\n }\n\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n\n entry.promise.then(\n (value) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n resolve(value);\n },\n (error) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n reject(error);\n },\n );\n });\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n for (const entry of this.inFlightRequests.values()) {\n if (entry.abortTimer) clearTimeout(entry.abortTimer);\n }\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.getValid(key);\n return entry !== null;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { getClientOptions } from \"../context/client-options\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { ApiError, createWorkspaceClient } from \"../workspace-client\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Reference-counted in-flight cache execution entry.\n *\n * `sharedController` decouples the cached `fn()` from any single caller's\n * abort signal. Callers join an in-flight entry by incrementing `refCount`;\n * when a caller aborts, refCount is decremented. The shared controller is\n * aborted only when refCount drops to 0 — i.e. all callers have abandoned\n * the request. This prevents one caller's cancellation (e.g. React\n * StrictMode unmount) from poisoning the in-flight result for other still-\n * connected awaiters.\n */\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n refCount: number;\n sharedController: AbortController;\n abortTimer?: ReturnType<typeof setTimeout>;\n}\n\nfunction createAbortError(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason;\n return new DOMException(\"The operation was aborted.\", \"AbortError\");\n}\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private static readonly ABORT_GRACE_PERIOD_MS = 100;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, InFlightEntry<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = createWorkspaceClient({\n clientOptions: getClientOptions(),\n }).toLegacyWorkspaceClient();\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result.\n *\n * Multiple concurrent callers with the same `cacheKey` are deduplicated\n * onto a single in-flight execution. Each caller may pass its own\n * `callerSignal`; the underlying `fn()` is run with a shared, internally\n * managed `AbortSignal` that aborts only when *all* callers have\n * abandoned the request (reference counted). This decouples a single\n * caller's cancellation (e.g. React StrictMode unmount) from the shared\n * result, so other still-connected callers receive the cached value\n * normally.\n *\n * @param key - Cache key parts\n * @param fn - Function to execute. Receives the cache-owned shared signal;\n * pass it through to the underlying I/O so the work is cancelled when\n * no caller is left waiting.\n * @param userKey - User key for cache namespacing\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: (sharedSignal?: AbortSignal) => Promise<T>,\n userKey: string,\n options?: { ttl?: number; callerSignal?: AbortSignal },\n ): Promise<T> {\n if (!this.config.enabled) return fn(options?.callerSignal);\n\n const callerSignal = options?.callerSignal;\n if (callerSignal?.aborted) {\n throw createAbortError(callerSignal);\n }\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n const cached = await this.getValid<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value;\n }\n\n // check if the value is being processed by another request — join\n // the existing in-flight entry under reference counting so this\n // caller's abort doesn't poison the shared result.\n const existing = this.inFlightRequests.get(cacheKey) as\n | InFlightEntry<T>\n | undefined;\n if (existing && !existing.sharedController.signal.aborted) {\n existing.refCount++;\n // Cancel any pending abort timer — a new caller has joined\n if (existing.abortTimer) {\n clearTimeout(existing.abortTimer);\n existing.abortTimer = undefined;\n }\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n return await this._waitWithRefCount(existing, callerSignal);\n }\n\n // cache miss - execute function under a shared abort controller\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const sharedController = new AbortController();\n const entry: InFlightEntry<T> = {\n promise: undefined as unknown as Promise<T>,\n refCount: 1,\n sharedController,\n };\n\n entry.promise = fn(sharedController.signal)\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // If the shared controller aborted, all callers have already\n // abandoned the request (or are about to via their own signals)\n // — propagate the original error without wrapping. No live\n // awaiter will observe this rejection.\n if (sharedController.signal.aborted) {\n throw error;\n }\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n if (this.inFlightRequests.get(cacheKey) === entry) {\n this.inFlightRequests.delete(cacheKey);\n }\n });\n\n // Suppress unhandled rejection warnings when every caller bailed\n // before fn() resolved (their own promises rejected via\n // waitWithRefCount; the underlying entry.promise has no awaiter).\n entry.promise.catch(() => {});\n\n this.inFlightRequests.set(cacheKey, entry as InFlightEntry<unknown>);\n\n const result = await this._waitWithRefCount(entry, callerSignal);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait on an in-flight entry, racing the underlying promise against the\n * caller's abort signal. When the caller aborts, the entry's refCount is\n * decremented; if it hits zero the shared controller is aborted so the\n * underlying `fn()` can stop. Other callers continue to await the same\n * entry and receive the result when it arrives.\n */\n private _waitWithRefCount<T>(\n entry: InFlightEntry<T>,\n callerSignal?: AbortSignal,\n ): Promise<T> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const release = () => {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n // Grace period: delay abort so a StrictMode remount can join\n // the in-flight entry before the shared execution is cancelled.\n entry.abortTimer = setTimeout(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\n callerSignal.reason ?? \"all cache callers aborted\",\n );\n }\n }, CacheManager.ABORT_GRACE_PERIOD_MS);\n }\n };\n\n const onAbort = () => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n release();\n reject(createAbortError(callerSignal));\n };\n\n if (callerSignal.aborted) {\n onAbort();\n return;\n }\n\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n\n entry.promise.then(\n (value) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n resolve(value);\n },\n (error) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n reject(error);\n },\n );\n });\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n for (const entry of this.inFlightRequests.values()) {\n if (entry.abortTimer) clearTimeout(entry.abortTimer);\n }\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.getValid(key);\n return entry !== null;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
@@ -1,8 +1,9 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
+ import { ApiError } from "../../workspace-client/errors.js";
3
+ import "../../workspace-client/index.js";
2
4
  import { TelemetryManager } from "../../telemetry/telemetry-manager.js";
3
5
  import { SpanKind, SpanStatusCode } from "../../telemetry/index.js";
4
6
  import { FILES_MAX_READ_SIZE, contentTypeFromPath, isTextContentType } from "./defaults.js";
5
- import { ApiError } from "@databricks/sdk-experimental";
6
7
  import { AsyncLocalStorage } from "node:async_hooks";
7
8
 
8
9
  //#region src/connectors/files/client.ts
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/files/client.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { ApiError, type WorkspaceClient } from \"@databricks/sdk-experimental\";\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 {\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":"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"}
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":"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"}
@@ -1,4 +1,2 @@
1
- import "./types.js";
2
- import { WorkspaceClient } from "@databricks/sdk-experimental";
3
- import "@databricks/sdk-experimental/dist/apis/dashboards";
4
- import "@databricks/sdk-experimental/dist/wait";
1
+ import "../../workspace-client/index.js";
2
+ import "./types.js";
@@ -1,12 +1,11 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
+ import { Time, TimeUnits } from "../../workspace-client/legacy.js";
3
+ import "../../workspace-client/index.js";
2
4
  import "../../logging/index.js";
3
5
  import { genieConnectorDefaults } from "./defaults.js";
4
6
  import { pollWaiter } from "./poll-waiter.js";
5
- import * as SDK from "@databricks/sdk-experimental";
6
7
 
7
8
  //#region src/connectors/genie/client.ts
8
- const { TimeUnits } = SDK;
9
- const Time = SDK.Time ?? SDK.default.Time;
10
9
  const logger = createLogger("connectors:genie");
11
10
  const GenieErrors = {
12
11
  SPACE_ACCESS_DENIED: "You don't have access to this Genie Space.",
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/genie/client.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport * as SDK from \"@databricks/sdk-experimental\";\nimport type { GenieMessage } from \"@databricks/sdk-experimental/dist/apis/dashboards\";\nimport type { Waiter } from \"@databricks/sdk-experimental/dist/wait\";\nimport { createLogger } from \"../../logging\";\nimport { genieConnectorDefaults } from \"./defaults\";\nimport { pollWaiter } from \"./poll-waiter\";\nimport type {\n GenieAttachmentResponse,\n GenieConversationHistoryResponse,\n GenieMessageResponse,\n GenieStatementResponse,\n GenieStreamEvent,\n} from \"./types\";\n\nconst { TimeUnits } = SDK;\nconst Time = SDK.Time ?? (SDK as any).default.Time;\n\nconst logger = createLogger(\"connectors:genie\");\n\nconst GenieErrors = {\n SPACE_ACCESS_DENIED: \"You don't have access to this Genie Space.\",\n TABLE_PERMISSIONS:\n \"You may not have access to the data tables. Please verify your table permissions.\",\n REQUEST_FAILED: \"Genie request failed\",\n QUERY_RESULT_FAILED: \"Failed to fetch query result\",\n} as const;\n\ntype CreateMessageWaiter = Waiter<GenieMessage, GenieMessage>;\n\ninterface GenieConnectorConfig {\n timeout?: number;\n maxMessages?: number;\n}\n\nfunction mapAttachments(message: GenieMessage): GenieAttachmentResponse[] {\n return (\n message.attachments?.map((att) => ({\n attachmentId: att.attachment_id,\n query: att.query\n ? {\n title: att.query.title,\n description: att.query.description,\n query: att.query.query,\n statementId: att.query.statement_id,\n }\n : undefined,\n text: att.text ? { content: att.text.content } : undefined,\n suggestedQuestions: att.suggested_questions?.questions,\n })) ?? []\n );\n}\n\nfunction toMessageResponse(message: GenieMessage): GenieMessageResponse {\n return {\n messageId: message.message_id,\n conversationId: message.conversation_id,\n spaceId: message.space_id,\n status: message.status ?? \"COMPLETED\",\n content: message.content,\n attachments: mapAttachments(message),\n error: message.error?.error,\n };\n}\n\nfunction classifyGenieError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n\n if (message.includes(\"RESOURCE_DOES_NOT_EXIST\")) {\n return GenieErrors.SPACE_ACCESS_DENIED;\n }\n\n if (\n message.includes(\"failed to reach COMPLETED state\") &&\n message.includes(\"FAILED\")\n ) {\n return GenieErrors.TABLE_PERMISSIONS;\n }\n\n return message || GenieErrors.REQUEST_FAILED;\n}\n\nexport class GenieConnector {\n private readonly config: Required<GenieConnectorConfig>;\n\n constructor(config: GenieConnectorConfig = {}) {\n this.config = {\n timeout: config.timeout ?? genieConnectorDefaults.timeout,\n maxMessages: config.maxMessages ?? genieConnectorDefaults.maxMessages,\n };\n }\n\n async startMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<{\n messageWaiter: CreateMessageWaiter;\n conversationId: string;\n messageId: string;\n }> {\n if (conversationId) {\n const waiter = await workspaceClient.genie.createMessage({\n space_id: spaceId,\n conversation_id: conversationId,\n content,\n });\n return {\n messageWaiter: waiter,\n conversationId,\n messageId: waiter.message_id ?? \"\",\n };\n }\n const start = await workspaceClient.genie.startConversation({\n space_id: spaceId,\n content,\n });\n return {\n messageWaiter: start as unknown as CreateMessageWaiter,\n conversationId: start.conversation_id,\n messageId: start.message_id,\n };\n }\n\n async waitForMessage(\n messageWaiter: CreateMessageWaiter,\n options?: { timeout?: number },\n ): Promise<GenieMessage> {\n const timeout = options?.timeout ?? this.config.timeout;\n const waitOptions =\n timeout > 0 ? { timeout: new Time(timeout, TimeUnits.milliseconds) } : {};\n return messageWaiter.wait(waitOptions);\n }\n\n async listConversationMessages(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: { pageSize?: number; pageToken?: string },\n ): Promise<{\n messages: GenieMessageResponse[];\n nextPageToken: string | null;\n }> {\n const pageSize =\n options?.pageSize ?? genieConnectorDefaults.initialPageSize;\n\n const response = await workspaceClient.genie.listConversationMessages({\n space_id: spaceId,\n conversation_id: conversationId,\n page_size: pageSize,\n ...(options?.pageToken ? { page_token: options.pageToken } : {}),\n });\n\n const messages = (response.messages ?? []).reverse().map(toMessageResponse);\n\n return {\n messages,\n nextPageToken: response.next_page_token ?? null,\n };\n }\n\n async getMessageAttachmentQueryResult(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n attachmentId: string,\n _signal?: AbortSignal,\n ): Promise<GenieStatementResponse> {\n const response =\n await workspaceClient.genie.getMessageAttachmentQueryResult({\n space_id: spaceId,\n conversation_id: conversationId,\n message_id: messageId,\n attachment_id: attachmentId,\n });\n return response.statement_response as GenieStatementResponse;\n }\n\n async *streamSendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n options?: { timeout?: number; signal?: AbortSignal },\n ): AsyncGenerator<GenieStreamEvent> {\n try {\n const {\n messageWaiter,\n conversationId: resultConversationId,\n messageId: resultMessageId,\n } = await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n\n yield {\n type: \"message_start\",\n conversationId: resultConversationId,\n messageId: resultMessageId,\n spaceId,\n };\n\n const timeout =\n options?.timeout != null ? options.timeout : this.config.timeout;\n const waitOptions =\n timeout > 0\n ? { timeout: new Time(timeout, TimeUnits.milliseconds) }\n : {};\n\n let completedMessage!: GenieMessage;\n for await (const event of pollWaiter(messageWaiter, waitOptions)) {\n if (event.type === \"progress\" && event.value.status) {\n yield { type: \"status\", status: event.value.status };\n } else if (event.type === \"completed\") {\n completedMessage = event.value;\n }\n }\n\n const messageResponse = toMessageResponse(completedMessage);\n yield { type: \"message_result\", message: messageResponse };\n\n yield* this.emitQueryResults(\n workspaceClient,\n spaceId,\n resultConversationId,\n messageResponse.messageId,\n messageResponse,\n );\n } catch (error) {\n logger.error(\n \"Genie message error (spaceId=%s, conversationId=%s): %O\",\n spaceId,\n conversationId ?? \"new\",\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n private async *emitQueryResults(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n messageResponse: GenieMessageResponse,\n ): AsyncGenerator<\n Extract<GenieStreamEvent, { type: \"query_result\" } | { type: \"error\" }>\n > {\n const attachments = messageResponse.attachments ?? [];\n for (const att of attachments) {\n if (!att.query?.statementId || !att.attachmentId) continue;\n try {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n messageId,\n att.attachmentId,\n );\n yield {\n type: \"query_result\",\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n data,\n };\n } catch (error) {\n logger.error(\n \"Failed to fetch query result for attachment %s: %O\",\n att.attachmentId,\n error,\n );\n yield {\n type: \"error\",\n error: `${GenieErrors.QUERY_RESULT_FAILED} for attachment ${att.attachmentId}`,\n };\n }\n }\n }\n\n async *streamConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: {\n includeQueryResults?: boolean;\n pageSize?: number;\n pageToken?: string;\n signal?: AbortSignal;\n },\n ): AsyncGenerator<GenieStreamEvent> {\n const includeQueryResults = options?.includeQueryResults !== false;\n\n try {\n const { messages: messageResponses, nextPageToken } =\n await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n { pageSize: options?.pageSize, pageToken: options?.pageToken },\n );\n\n for (const messageResponse of messageResponses) {\n yield { type: \"message_result\", message: messageResponse };\n }\n\n yield {\n type: \"history_info\",\n conversationId,\n spaceId,\n nextPageToken,\n loadedCount: messageResponses.length,\n };\n\n if (includeQueryResults) {\n const queryAttachments: Array<{\n messageId: string;\n attachmentId: string;\n statementId: string;\n }> = [];\n\n for (const msg of messageResponses) {\n for (const att of msg.attachments ?? []) {\n if (att.query?.statementId && att.attachmentId) {\n queryAttachments.push({\n messageId: msg.messageId,\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n });\n }\n }\n }\n\n const results = await Promise.allSettled(\n queryAttachments.map(async (att) => {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n att.messageId,\n att.attachmentId,\n );\n return {\n attachmentId: att.attachmentId,\n statementId: att.statementId,\n data,\n };\n }),\n );\n\n for (const result of results) {\n if (result.status === \"fulfilled\") {\n yield {\n type: \"query_result\",\n attachmentId: result.value.attachmentId,\n statementId: result.value.statementId,\n data: result.value.data,\n };\n } else {\n logger.error(\"Failed to fetch query result: %O\", result.reason);\n yield {\n type: \"error\",\n error:\n result.reason instanceof Error\n ? result.reason.message\n : GenieErrors.QUERY_RESULT_FAILED,\n };\n }\n }\n }\n } catch (error) {\n logger.error(\n \"Genie getConversation error (spaceId=%s, conversationId=%s): %O\",\n spaceId,\n conversationId,\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n /**\n * Polls a single message via `getMessage` until it reaches a terminal\n * state (`COMPLETED` or `FAILED`). Yields the same event types as\n * `streamSendMessage` so callers can reuse the same SSE processing logic.\n */\n async *streamGetMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n options?: { timeout?: number; pollInterval?: number; signal?: AbortSignal },\n ): AsyncGenerator<GenieStreamEvent> {\n const pollInterval = options?.pollInterval ?? 3_000;\n const signal = options?.signal;\n let lastStatus = \"\";\n\n try {\n while (true) {\n if (signal?.aborted) return;\n\n const message = await workspaceClient.genie.getMessage({\n space_id: spaceId,\n conversation_id: conversationId,\n message_id: messageId,\n });\n\n if (message.status && message.status !== lastStatus) {\n lastStatus = message.status;\n yield { type: \"status\", status: message.status };\n }\n\n const isTerminal =\n message.status === \"COMPLETED\" || message.status === \"FAILED\";\n if (isTerminal) {\n const messageResponse = toMessageResponse(message);\n yield { type: \"message_result\", message: messageResponse };\n yield* this.emitQueryResults(\n workspaceClient,\n spaceId,\n conversationId,\n messageId,\n messageResponse,\n );\n return;\n }\n\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, pollInterval);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n }\n } catch (error) {\n if (signal?.aborted) return;\n logger.error(\n \"Genie getMessage poll error (spaceId=%s, conversationId=%s, messageId=%s): %O\",\n spaceId,\n conversationId,\n messageId,\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n async sendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<GenieMessageResponse> {\n const { messageWaiter, conversationId: resultConversationId } =\n await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n const completedMessage = await this.waitForMessage(messageWaiter);\n const messageResponse = toMessageResponse(completedMessage);\n return {\n ...messageResponse,\n conversationId: resultConversationId,\n };\n }\n\n async getConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n ): Promise<GenieConversationHistoryResponse> {\n const allMessages: GenieMessageResponse[] = [];\n let pageToken: string | undefined;\n\n do {\n const { messages, nextPageToken } = await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n {\n pageSize: genieConnectorDefaults.pageSize,\n pageToken,\n },\n );\n allMessages.push(...messages);\n pageToken = nextPageToken ?? undefined;\n } while (pageToken && allMessages.length < this.config.maxMessages);\n\n return {\n conversationId,\n spaceId,\n messages: allMessages.slice(0, this.config.maxMessages),\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/genie/client.ts"],"sourcesContent":["import { createLogger } from \"../../logging\";\nimport {\n type GenieMessage,\n Time,\n TimeUnits,\n type Waiter,\n type WorkspaceClient,\n} from \"../../workspace-client\";\nimport { genieConnectorDefaults } from \"./defaults\";\nimport { pollWaiter } from \"./poll-waiter\";\nimport type {\n GenieAttachmentResponse,\n GenieConversationHistoryResponse,\n GenieMessageResponse,\n GenieStatementResponse,\n GenieStreamEvent,\n} from \"./types\";\n\nconst logger = createLogger(\"connectors:genie\");\n\nconst GenieErrors = {\n SPACE_ACCESS_DENIED: \"You don't have access to this Genie Space.\",\n TABLE_PERMISSIONS:\n \"You may not have access to the data tables. Please verify your table permissions.\",\n REQUEST_FAILED: \"Genie request failed\",\n QUERY_RESULT_FAILED: \"Failed to fetch query result\",\n} as const;\n\ntype CreateMessageWaiter = Waiter<GenieMessage, GenieMessage>;\n\ninterface GenieConnectorConfig {\n timeout?: number;\n maxMessages?: number;\n}\n\nfunction mapAttachments(message: GenieMessage): GenieAttachmentResponse[] {\n return (\n message.attachments?.map((att) => ({\n attachmentId: att.attachment_id,\n query: att.query\n ? {\n title: att.query.title,\n description: att.query.description,\n query: att.query.query,\n statementId: att.query.statement_id,\n }\n : undefined,\n text: att.text ? { content: att.text.content } : undefined,\n suggestedQuestions: att.suggested_questions?.questions,\n })) ?? []\n );\n}\n\nfunction toMessageResponse(message: GenieMessage): GenieMessageResponse {\n return {\n messageId: message.message_id,\n conversationId: message.conversation_id,\n spaceId: message.space_id,\n status: message.status ?? \"COMPLETED\",\n content: message.content,\n attachments: mapAttachments(message),\n error: message.error?.error,\n };\n}\n\nfunction classifyGenieError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n\n if (message.includes(\"RESOURCE_DOES_NOT_EXIST\")) {\n return GenieErrors.SPACE_ACCESS_DENIED;\n }\n\n if (\n message.includes(\"failed to reach COMPLETED state\") &&\n message.includes(\"FAILED\")\n ) {\n return GenieErrors.TABLE_PERMISSIONS;\n }\n\n return message || GenieErrors.REQUEST_FAILED;\n}\n\nexport class GenieConnector {\n private readonly config: Required<GenieConnectorConfig>;\n\n constructor(config: GenieConnectorConfig = {}) {\n this.config = {\n timeout: config.timeout ?? genieConnectorDefaults.timeout,\n maxMessages: config.maxMessages ?? genieConnectorDefaults.maxMessages,\n };\n }\n\n async startMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<{\n messageWaiter: CreateMessageWaiter;\n conversationId: string;\n messageId: string;\n }> {\n if (conversationId) {\n const waiter = await workspaceClient.genie.createMessage({\n space_id: spaceId,\n conversation_id: conversationId,\n content,\n });\n return {\n messageWaiter: waiter,\n conversationId,\n messageId: waiter.message_id ?? \"\",\n };\n }\n const start = await workspaceClient.genie.startConversation({\n space_id: spaceId,\n content,\n });\n return {\n messageWaiter: start as unknown as CreateMessageWaiter,\n conversationId: start.conversation_id,\n messageId: start.message_id,\n };\n }\n\n async waitForMessage(\n messageWaiter: CreateMessageWaiter,\n options?: { timeout?: number },\n ): Promise<GenieMessage> {\n const timeout = options?.timeout ?? this.config.timeout;\n const waitOptions =\n timeout > 0 ? { timeout: new Time(timeout, TimeUnits.milliseconds) } : {};\n return messageWaiter.wait(waitOptions);\n }\n\n async listConversationMessages(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: { pageSize?: number; pageToken?: string },\n ): Promise<{\n messages: GenieMessageResponse[];\n nextPageToken: string | null;\n }> {\n const pageSize =\n options?.pageSize ?? genieConnectorDefaults.initialPageSize;\n\n const response = await workspaceClient.genie.listConversationMessages({\n space_id: spaceId,\n conversation_id: conversationId,\n page_size: pageSize,\n ...(options?.pageToken ? { page_token: options.pageToken } : {}),\n });\n\n const messages = (response.messages ?? []).reverse().map(toMessageResponse);\n\n return {\n messages,\n nextPageToken: response.next_page_token ?? null,\n };\n }\n\n async getMessageAttachmentQueryResult(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n attachmentId: string,\n _signal?: AbortSignal,\n ): Promise<GenieStatementResponse> {\n const response =\n await workspaceClient.genie.getMessageAttachmentQueryResult({\n space_id: spaceId,\n conversation_id: conversationId,\n message_id: messageId,\n attachment_id: attachmentId,\n });\n return response.statement_response as GenieStatementResponse;\n }\n\n async *streamSendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n options?: { timeout?: number; signal?: AbortSignal },\n ): AsyncGenerator<GenieStreamEvent> {\n try {\n const {\n messageWaiter,\n conversationId: resultConversationId,\n messageId: resultMessageId,\n } = await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n\n yield {\n type: \"message_start\",\n conversationId: resultConversationId,\n messageId: resultMessageId,\n spaceId,\n };\n\n const timeout =\n options?.timeout != null ? options.timeout : this.config.timeout;\n const waitOptions =\n timeout > 0\n ? { timeout: new Time(timeout, TimeUnits.milliseconds) }\n : {};\n\n let completedMessage!: GenieMessage;\n for await (const event of pollWaiter(messageWaiter, waitOptions)) {\n if (event.type === \"progress\" && event.value.status) {\n yield { type: \"status\", status: event.value.status };\n } else if (event.type === \"completed\") {\n completedMessage = event.value;\n }\n }\n\n const messageResponse = toMessageResponse(completedMessage);\n yield { type: \"message_result\", message: messageResponse };\n\n yield* this.emitQueryResults(\n workspaceClient,\n spaceId,\n resultConversationId,\n messageResponse.messageId,\n messageResponse,\n );\n } catch (error) {\n logger.error(\n \"Genie message error (spaceId=%s, conversationId=%s): %O\",\n spaceId,\n conversationId ?? \"new\",\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n private async *emitQueryResults(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n messageResponse: GenieMessageResponse,\n ): AsyncGenerator<\n Extract<GenieStreamEvent, { type: \"query_result\" } | { type: \"error\" }>\n > {\n const attachments = messageResponse.attachments ?? [];\n for (const att of attachments) {\n if (!att.query?.statementId || !att.attachmentId) continue;\n try {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n messageId,\n att.attachmentId,\n );\n yield {\n type: \"query_result\",\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n data,\n };\n } catch (error) {\n logger.error(\n \"Failed to fetch query result for attachment %s: %O\",\n att.attachmentId,\n error,\n );\n yield {\n type: \"error\",\n error: `${GenieErrors.QUERY_RESULT_FAILED} for attachment ${att.attachmentId}`,\n };\n }\n }\n }\n\n async *streamConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n options?: {\n includeQueryResults?: boolean;\n pageSize?: number;\n pageToken?: string;\n signal?: AbortSignal;\n },\n ): AsyncGenerator<GenieStreamEvent> {\n const includeQueryResults = options?.includeQueryResults !== false;\n\n try {\n const { messages: messageResponses, nextPageToken } =\n await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n { pageSize: options?.pageSize, pageToken: options?.pageToken },\n );\n\n for (const messageResponse of messageResponses) {\n yield { type: \"message_result\", message: messageResponse };\n }\n\n yield {\n type: \"history_info\",\n conversationId,\n spaceId,\n nextPageToken,\n loadedCount: messageResponses.length,\n };\n\n if (includeQueryResults) {\n const queryAttachments: Array<{\n messageId: string;\n attachmentId: string;\n statementId: string;\n }> = [];\n\n for (const msg of messageResponses) {\n for (const att of msg.attachments ?? []) {\n if (att.query?.statementId && att.attachmentId) {\n queryAttachments.push({\n messageId: msg.messageId,\n attachmentId: att.attachmentId,\n statementId: att.query.statementId,\n });\n }\n }\n }\n\n const results = await Promise.allSettled(\n queryAttachments.map(async (att) => {\n const data = await this.getMessageAttachmentQueryResult(\n workspaceClient,\n spaceId,\n conversationId,\n att.messageId,\n att.attachmentId,\n );\n return {\n attachmentId: att.attachmentId,\n statementId: att.statementId,\n data,\n };\n }),\n );\n\n for (const result of results) {\n if (result.status === \"fulfilled\") {\n yield {\n type: \"query_result\",\n attachmentId: result.value.attachmentId,\n statementId: result.value.statementId,\n data: result.value.data,\n };\n } else {\n logger.error(\"Failed to fetch query result: %O\", result.reason);\n yield {\n type: \"error\",\n error:\n result.reason instanceof Error\n ? result.reason.message\n : GenieErrors.QUERY_RESULT_FAILED,\n };\n }\n }\n }\n } catch (error) {\n logger.error(\n \"Genie getConversation error (spaceId=%s, conversationId=%s): %O\",\n spaceId,\n conversationId,\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n /**\n * Polls a single message via `getMessage` until it reaches a terminal\n * state (`COMPLETED` or `FAILED`). Yields the same event types as\n * `streamSendMessage` so callers can reuse the same SSE processing logic.\n */\n async *streamGetMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n messageId: string,\n options?: { timeout?: number; pollInterval?: number; signal?: AbortSignal },\n ): AsyncGenerator<GenieStreamEvent> {\n const pollInterval = options?.pollInterval ?? 3_000;\n const signal = options?.signal;\n let lastStatus = \"\";\n\n try {\n while (true) {\n if (signal?.aborted) return;\n\n const message = await workspaceClient.genie.getMessage({\n space_id: spaceId,\n conversation_id: conversationId,\n message_id: messageId,\n });\n\n if (message.status && message.status !== lastStatus) {\n lastStatus = message.status;\n yield { type: \"status\", status: message.status };\n }\n\n const isTerminal =\n message.status === \"COMPLETED\" || message.status === \"FAILED\";\n if (isTerminal) {\n const messageResponse = toMessageResponse(message);\n yield { type: \"message_result\", message: messageResponse };\n yield* this.emitQueryResults(\n workspaceClient,\n spaceId,\n conversationId,\n messageId,\n messageResponse,\n );\n return;\n }\n\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, pollInterval);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n }\n } catch (error) {\n if (signal?.aborted) return;\n logger.error(\n \"Genie getMessage poll error (spaceId=%s, conversationId=%s, messageId=%s): %O\",\n spaceId,\n conversationId,\n messageId,\n error,\n );\n yield { type: \"error\", error: classifyGenieError(error) };\n }\n }\n\n async sendMessage(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n content: string,\n conversationId: string | undefined,\n ): Promise<GenieMessageResponse> {\n const { messageWaiter, conversationId: resultConversationId } =\n await this.startMessage(\n workspaceClient,\n spaceId,\n content,\n conversationId,\n );\n const completedMessage = await this.waitForMessage(messageWaiter);\n const messageResponse = toMessageResponse(completedMessage);\n return {\n ...messageResponse,\n conversationId: resultConversationId,\n };\n }\n\n async getConversation(\n workspaceClient: WorkspaceClient,\n spaceId: string,\n conversationId: string,\n ): Promise<GenieConversationHistoryResponse> {\n const allMessages: GenieMessageResponse[] = [];\n let pageToken: string | undefined;\n\n do {\n const { messages, nextPageToken } = await this.listConversationMessages(\n workspaceClient,\n spaceId,\n conversationId,\n {\n pageSize: genieConnectorDefaults.pageSize,\n pageToken,\n },\n );\n allMessages.push(...messages);\n pageToken = nextPageToken ?? undefined;\n } while (pageToken && allMessages.length < this.config.maxMessages);\n\n return {\n conversationId,\n spaceId,\n messages: allMessages.slice(0, this.config.maxMessages),\n };\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import "./types.js";
2
- import { WorkspaceClient, jobs } from "@databricks/sdk-experimental";
1
+ import "../../workspace-client/index.js";
2
+ import "./types.js";
@@ -2,9 +2,10 @@ import { createLogger } from "../../logging/logger.js";
2
2
  import { AppKitError } from "../../errors/base.js";
3
3
  import { ExecutionError } from "../../errors/execution.js";
4
4
  import "../../errors/index.js";
5
+ import { Context } from "../../workspace-client/legacy.js";
6
+ import "../../workspace-client/index.js";
5
7
  import { TelemetryManager } from "../../telemetry/telemetry-manager.js";
6
8
  import { SpanKind, SpanStatusCode } from "../../telemetry/index.js";
7
- import { Context } from "@databricks/sdk-experimental";
8
9
 
9
10
  //#region src/connectors/jobs/client.ts
10
11
  const logger = createLogger("connectors:jobs");
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/jobs/client.ts"],"sourcesContent":["import {\n Context,\n type jobs,\n type WorkspaceClient,\n} from \"@databricks/sdk-experimental\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\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 type { JobsConnectorConfig } from \"./types\";\n\nconst logger = createLogger(\"connectors:jobs\");\n\nexport class JobsConnector {\n private readonly name = \"jobs\";\n private readonly config: JobsConnectorConfig;\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n apiCallCount: Counter;\n apiCallDuration: Histogram;\n };\n\n constructor(config: JobsConnectorConfig) {\n this.config = config;\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n apiCallCount: this.telemetry\n .getMeter()\n .createCounter(\"jobs.api_call.count\", {\n description: \"Total number of Jobs API calls\",\n unit: \"1\",\n }),\n apiCallDuration: this.telemetry\n .getMeter()\n .createHistogram(\"jobs.api_call.duration\", {\n description: \"Duration of Jobs API calls\",\n unit: \"ms\",\n }),\n };\n }\n\n async submitRun(\n workspaceClient: WorkspaceClient,\n request: jobs.SubmitRun,\n signal?: AbortSignal,\n ): Promise<jobs.SubmitRunResponse> {\n return this._callApi(\"submit\", async () => {\n return workspaceClient.jobs.submit(request, this._createContext(signal));\n });\n }\n\n async runNow(\n workspaceClient: WorkspaceClient,\n request: jobs.RunNow,\n signal?: AbortSignal,\n ): Promise<jobs.RunNowResponse> {\n return this._callApi(\"runNow\", async () => {\n return workspaceClient.jobs.runNow(request, this._createContext(signal));\n });\n }\n\n async getRun(\n workspaceClient: WorkspaceClient,\n request: jobs.GetRunRequest,\n signal?: AbortSignal,\n ): Promise<jobs.Run> {\n return this._callApi(\"getRun\", async () => {\n return workspaceClient.jobs.getRun(request, this._createContext(signal));\n });\n }\n\n async getRunOutput(\n workspaceClient: WorkspaceClient,\n request: jobs.GetRunOutputRequest,\n signal?: AbortSignal,\n ): Promise<jobs.RunOutput> {\n return this._callApi(\"getRunOutput\", async () => {\n return workspaceClient.jobs.getRunOutput(\n request,\n this._createContext(signal),\n );\n });\n }\n\n async cancelRun(\n workspaceClient: WorkspaceClient,\n request: jobs.CancelRun,\n signal?: AbortSignal,\n ): Promise<void> {\n await this._callApi(\"cancelRun\", async () => {\n return workspaceClient.jobs.cancelRun(\n request,\n this._createContext(signal),\n );\n });\n }\n\n async listRuns(\n workspaceClient: WorkspaceClient,\n request: jobs.ListRunsRequest,\n signal?: AbortSignal,\n ): Promise<jobs.BaseRun[]> {\n return this._callApi(\"listRuns\", async () => {\n const runs: jobs.BaseRun[] = [];\n const limit = Math.max(1, Math.min(request.limit ?? 100, 100));\n for await (const run of workspaceClient.jobs.listRuns(\n { ...request, limit },\n this._createContext(signal),\n )) {\n runs.push(run);\n if (runs.length >= limit) break;\n }\n return runs;\n });\n }\n\n async getJob(\n workspaceClient: WorkspaceClient,\n request: jobs.GetJobRequest,\n signal?: AbortSignal,\n ): Promise<jobs.Job> {\n return this._callApi(\"getJob\", async () => {\n return workspaceClient.jobs.get(request, this._createContext(signal));\n });\n }\n\n private async _callApi<T>(\n operation: string,\n fn: () => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n return this.telemetry.startActiveSpan(\n `jobs.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"jobs.operation\": operation,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn();\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 if (error instanceof AppKitError) {\n throw error;\n }\n // Preserve SDK ApiError (and any error with a numeric statusCode)\n // so Plugin.execute() can map it to the correct HTTP status.\n if (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n ) {\n throw error;\n }\n throw new ExecutionError(\n `Jobs API call failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n } finally {\n const duration = Date.now() - startTime;\n span.end();\n this.telemetryMetrics.apiCallCount.add(1, {\n operation,\n success: success.toString(),\n });\n this.telemetryMetrics.apiCallDuration.record(duration, {\n operation,\n success: success.toString(),\n });\n\n logger.event()?.setContext(\"jobs\", {\n operation,\n duration_ms: duration,\n success,\n });\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n private _createContext(signal?: AbortSignal) {\n return new Context({\n cancellationToken: {\n // Getter — evaluated on every read so SDK code paths that poll\n // (rather than subscribe) observe cancellation live.\n get isCancellationRequested() {\n return signal?.aborted ?? false;\n },\n onCancellationRequested: (cb: () => void) => {\n signal?.addEventListener(\"abort\", cb, { once: true });\n },\n },\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/jobs/client.ts"],"sourcesContent":["import { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\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 jobs,\n type WorkspaceClient,\n} from \"../../workspace-client\";\nimport type { JobsConnectorConfig } from \"./types\";\n\nconst logger = createLogger(\"connectors:jobs\");\n\nexport class JobsConnector {\n private readonly name = \"jobs\";\n private readonly config: JobsConnectorConfig;\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n apiCallCount: Counter;\n apiCallDuration: Histogram;\n };\n\n constructor(config: JobsConnectorConfig) {\n this.config = config;\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n apiCallCount: this.telemetry\n .getMeter()\n .createCounter(\"jobs.api_call.count\", {\n description: \"Total number of Jobs API calls\",\n unit: \"1\",\n }),\n apiCallDuration: this.telemetry\n .getMeter()\n .createHistogram(\"jobs.api_call.duration\", {\n description: \"Duration of Jobs API calls\",\n unit: \"ms\",\n }),\n };\n }\n\n async submitRun(\n workspaceClient: WorkspaceClient,\n request: jobs.SubmitRun,\n signal?: AbortSignal,\n ): Promise<jobs.SubmitRunResponse> {\n return this._callApi(\"submit\", async () => {\n return workspaceClient.jobs.submit(request, this._createContext(signal));\n });\n }\n\n async runNow(\n workspaceClient: WorkspaceClient,\n request: jobs.RunNow,\n signal?: AbortSignal,\n ): Promise<jobs.RunNowResponse> {\n return this._callApi(\"runNow\", async () => {\n return workspaceClient.jobs.runNow(request, this._createContext(signal));\n });\n }\n\n async getRun(\n workspaceClient: WorkspaceClient,\n request: jobs.GetRunRequest,\n signal?: AbortSignal,\n ): Promise<jobs.Run> {\n return this._callApi(\"getRun\", async () => {\n return workspaceClient.jobs.getRun(request, this._createContext(signal));\n });\n }\n\n async getRunOutput(\n workspaceClient: WorkspaceClient,\n request: jobs.GetRunOutputRequest,\n signal?: AbortSignal,\n ): Promise<jobs.RunOutput> {\n return this._callApi(\"getRunOutput\", async () => {\n return workspaceClient.jobs.getRunOutput(\n request,\n this._createContext(signal),\n );\n });\n }\n\n async cancelRun(\n workspaceClient: WorkspaceClient,\n request: jobs.CancelRun,\n signal?: AbortSignal,\n ): Promise<void> {\n await this._callApi(\"cancelRun\", async () => {\n return workspaceClient.jobs.cancelRun(\n request,\n this._createContext(signal),\n );\n });\n }\n\n async listRuns(\n workspaceClient: WorkspaceClient,\n request: jobs.ListRunsRequest,\n signal?: AbortSignal,\n ): Promise<jobs.BaseRun[]> {\n return this._callApi(\"listRuns\", async () => {\n const runs: jobs.BaseRun[] = [];\n const limit = Math.max(1, Math.min(request.limit ?? 100, 100));\n for await (const run of workspaceClient.jobs.listRuns(\n { ...request, limit },\n this._createContext(signal),\n )) {\n runs.push(run);\n if (runs.length >= limit) break;\n }\n return runs;\n });\n }\n\n async getJob(\n workspaceClient: WorkspaceClient,\n request: jobs.GetJobRequest,\n signal?: AbortSignal,\n ): Promise<jobs.Job> {\n return this._callApi(\"getJob\", async () => {\n return workspaceClient.jobs.get(request, this._createContext(signal));\n });\n }\n\n private async _callApi<T>(\n operation: string,\n fn: () => Promise<T>,\n ): Promise<T> {\n const startTime = Date.now();\n let success = false;\n\n return this.telemetry.startActiveSpan(\n `jobs.${operation}`,\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"jobs.operation\": operation,\n },\n },\n async (span: Span) => {\n try {\n const result = await fn();\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 if (error instanceof AppKitError) {\n throw error;\n }\n // Preserve SDK ApiError (and any error with a numeric statusCode)\n // so Plugin.execute() can map it to the correct HTTP status.\n if (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n ) {\n throw error;\n }\n throw new ExecutionError(\n `Jobs API call failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n } finally {\n const duration = Date.now() - startTime;\n span.end();\n this.telemetryMetrics.apiCallCount.add(1, {\n operation,\n success: success.toString(),\n });\n this.telemetryMetrics.apiCallDuration.record(duration, {\n operation,\n success: success.toString(),\n });\n\n logger.event()?.setContext(\"jobs\", {\n operation,\n duration_ms: duration,\n success,\n });\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n private _createContext(signal?: AbortSignal) {\n return new Context({\n cancellationToken: {\n // Getter — evaluated on every read so SDK code paths that poll\n // (rather than subscribe) observe cancellation live.\n get isCancellationRequested() {\n return signal?.aborted ?? false;\n },\n onCancellationRequested: (cb: () => void) => {\n signal?.addEventListener(\"abort\", cb, { once: true });\n },\n },\n });\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { WorkspaceClient } from "@databricks/sdk-experimental";
1
+ import "../../workspace-client/index.js";
2
2
 
3
3
  //#region src/connectors/serving/client.d.ts
4
4
  /**
@@ -1,5 +1,6 @@
1
1
  import { createLogger } from "../../logging/logger.js";
2
- import { Context } from "@databricks/sdk-experimental";
2
+ import { Context } from "../../workspace-client/legacy.js";
3
+ import "../../workspace-client/index.js";
3
4
 
4
5
  //#region src/connectors/serving/client.ts
5
6
  const logger = createLogger("connectors:serving");
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/serving/client.ts"],"sourcesContent":["import type {\n CancellationToken,\n serving,\n WorkspaceClient,\n} from \"@databricks/sdk-experimental\";\nimport { Context } from \"@databricks/sdk-experimental\";\nimport { createLogger } from \"../../logging/logger\";\n\nconst logger = createLogger(\"connectors:serving\");\n\n/**\n * Bridges {@link AbortSignal} to the SDK's {@link CancellationToken} so\n * `apiClient.request` can abort the outbound HTTP request (and stop pulling\n * the SSE body) when the agent run is cancelled.\n */\nfunction cancellationTokenFromAbortSignal(\n signal: AbortSignal,\n): CancellationToken {\n const listeners = new Set<() => void>();\n const fire = () => {\n for (const cb of listeners) {\n try {\n cb();\n } catch {\n // ignore listener failures — abort must stay best-effort\n }\n }\n };\n signal.addEventListener(\"abort\", fire, { passive: true });\n\n return {\n get isCancellationRequested() {\n return signal.aborted;\n },\n onCancellationRequested(callback: (e?: unknown) => unknown) {\n listeners.add(callback as () => void);\n if (signal.aborted) {\n void callback();\n }\n },\n };\n}\n\n/**\n * Structural shape of a Databricks SDK client we need for the low-level\n * `apiClient.request` call. Lets `streamPath` be reused by adapters that\n * don't want a hard dependency on the concrete `WorkspaceClient` type.\n */\nexport interface ApiClientLike {\n apiClient: {\n request(\n options: Record<string, unknown>,\n context?: unknown,\n ): Promise<unknown>;\n };\n}\n\n/**\n * Transport shim shared by the agent adapters: given a request body, returns\n * the raw SSE byte stream from a serving / AI-gateway endpoint. Injected at\n * adapter construction time so callers can swap in the workspace SDK (the\n * factory paths via {@link streamPath}), a bare `fetch` (a reverse proxy /\n * mock), or a test fake.\n */\nexport type StreamBody = (\n body: Record<string, unknown>,\n signal?: AbortSignal,\n) => Promise<ReadableStream<Uint8Array>>;\n\n/**\n * Invokes a serving endpoint using the SDK's high-level query API.\n * Returns a typed QueryEndpointResponse.\n */\nexport async function invoke(\n client: WorkspaceClient,\n endpointName: string,\n body: Record<string, unknown>,\n): Promise<serving.QueryEndpointResponse> {\n // Strip `stream` from the body — the connector controls this\n const { stream: _stream, ...cleanBody } = body;\n\n logger.debug(\"Invoking endpoint %s\", endpointName);\n\n return client.servingEndpoints.query({\n name: endpointName,\n ...cleanBody,\n } as serving.QueryEndpointInput);\n}\n\n/**\n * POSTs `body` as JSON to an arbitrary workspace API path and returns the raw\n * SSE byte stream. No parsing is performed — bytes are passed through as-is.\n *\n * Uses the SDK's low-level `apiClient.request({ raw: true })` so callers\n * inherit URL resolution, the SDK credential chain (PAT/OAuth/OIDC), and\n * any future retries/telemetry baked into the SDK transport.\n *\n * When `signal` is provided it is bridged to the SDK's `Context` /\n * `CancellationToken` so aborts cancel the outbound HTTP request.\n *\n * @internal\n *\n * Not part of the public AppKit surface. `path` is passed through to the\n * SDK without any allowlist — exposing this to user-controlled input would\n * turn it into workspace-credentialled SSRF (CWE-918). Internal callers\n * must hard-code the path (or build it from a closed enum). New callers\n * inside the package: keep this constraint, and do not re-export from\n * `beta.ts` or any other entry point.\n */\nexport async function streamPath(\n client: ApiClientLike,\n path: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n): Promise<ReadableStream<Uint8Array>> {\n logger.debug(\"Streaming from path %s\", path);\n\n const context = signal\n ? new Context({\n cancellationToken: cancellationTokenFromAbortSignal(signal),\n })\n : undefined;\n\n const response = (await client.apiClient.request(\n {\n path,\n method: \"POST\",\n headers: new Headers({\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n }),\n payload: body,\n raw: true,\n },\n context,\n )) as { contents: ReadableStream<Uint8Array> | null };\n\n if (!response.contents) {\n throw new Error(\"Response body is null — streaming not supported\");\n }\n\n return response.contents;\n}\n\n/**\n * Returns the raw SSE byte stream from a serving endpoint. Thin wrapper over\n * {@link streamPath} that handles serving-specific URL encoding and forces\n * `stream: true` in the payload.\n */\nexport async function stream(\n client: WorkspaceClient,\n endpointName: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n): Promise<ReadableStream<Uint8Array>> {\n const { stream: _stream, ...cleanBody } = body;\n return streamPath(\n client as unknown as ApiClientLike,\n `/serving-endpoints/${encodeURIComponent(endpointName)}/invocations`,\n { ...cleanBody, stream: true },\n signal,\n );\n}\n"],"mappings":";;;;AAQA,MAAM,SAAS,aAAa,qBAAqB;;;;;;AAOjD,SAAS,iCACP,QACmB;CACnB,MAAM,4BAAY,IAAI,KAAiB;CACvC,MAAM,aAAa;AACjB,OAAK,MAAM,MAAM,UACf,KAAI;AACF,OAAI;UACE;;AAKZ,QAAO,iBAAiB,SAAS,MAAM,EAAE,SAAS,MAAM,CAAC;AAEzD,QAAO;EACL,IAAI,0BAA0B;AAC5B,UAAO,OAAO;;EAEhB,wBAAwB,UAAoC;AAC1D,aAAU,IAAI,SAAuB;AACrC,OAAI,OAAO,QACT,CAAK,UAAU;;EAGpB;;;;;;AAiCH,eAAsB,OACpB,QACA,cACA,MACwC;CAExC,MAAM,EAAE,QAAQ,SAAS,GAAG,cAAc;AAE1C,QAAO,MAAM,wBAAwB,aAAa;AAElD,QAAO,OAAO,iBAAiB,MAAM;EACnC,MAAM;EACN,GAAG;EACJ,CAA+B;;;;;;;;;;;;;;;;;;;;;;AAuBlC,eAAsB,WACpB,QACA,MACA,MACA,QACqC;AACrC,QAAO,MAAM,0BAA0B,KAAK;CAE5C,MAAM,UAAU,SACZ,IAAI,QAAQ,EACV,mBAAmB,iCAAiC,OAAO,EAC5D,CAAC,GACF;CAEJ,MAAM,WAAY,MAAM,OAAO,UAAU,QACvC;EACE;EACA,QAAQ;EACR,SAAS,IAAI,QAAQ;GACnB,gBAAgB;GAChB,QAAQ;GACT,CAAC;EACF,SAAS;EACT,KAAK;EACN,EACD,QACD;AAED,KAAI,CAAC,SAAS,SACZ,OAAM,IAAI,MAAM,kDAAkD;AAGpE,QAAO,SAAS;;;;;;;AAQlB,eAAsB,OACpB,QACA,cACA,MACA,QACqC;CACrC,MAAM,EAAE,QAAQ,SAAS,GAAG,cAAc;AAC1C,QAAO,WACL,QACA,sBAAsB,mBAAmB,aAAa,CAAC,eACvD;EAAE,GAAG;EAAW,QAAQ;EAAM,EAC9B,OACD"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/serving/client.ts"],"sourcesContent":["import { createLogger } from \"../../logging/logger\";\nimport {\n type CancellationToken,\n Context,\n type serving,\n type WorkspaceClient,\n} from \"../../workspace-client\";\n\nconst logger = createLogger(\"connectors:serving\");\n\n/**\n * Bridges {@link AbortSignal} to the SDK's {@link CancellationToken} so\n * `apiClient.request` can abort the outbound HTTP request (and stop pulling\n * the SSE body) when the agent run is cancelled.\n */\nfunction cancellationTokenFromAbortSignal(\n signal: AbortSignal,\n): CancellationToken {\n const listeners = new Set<() => void>();\n const fire = () => {\n for (const cb of listeners) {\n try {\n cb();\n } catch {\n // ignore listener failures — abort must stay best-effort\n }\n }\n };\n signal.addEventListener(\"abort\", fire, { passive: true });\n\n return {\n get isCancellationRequested() {\n return signal.aborted;\n },\n onCancellationRequested(callback: (e?: unknown) => unknown) {\n listeners.add(callback as () => void);\n if (signal.aborted) {\n void callback();\n }\n },\n };\n}\n\n/**\n * Structural shape of a Databricks SDK client we need for the low-level\n * `apiClient.request` call. Lets `streamPath` be reused by adapters that\n * don't want a hard dependency on the concrete `WorkspaceClient` type.\n */\nexport interface ApiClientLike {\n apiClient: {\n request(\n options: Record<string, unknown>,\n context?: unknown,\n ): Promise<unknown>;\n };\n}\n\n/**\n * Transport shim shared by the agent adapters: given a request body, returns\n * the raw SSE byte stream from a serving / AI-gateway endpoint. Injected at\n * adapter construction time so callers can swap in the workspace SDK (the\n * factory paths via {@link streamPath}), a bare `fetch` (a reverse proxy /\n * mock), or a test fake.\n */\nexport type StreamBody = (\n body: Record<string, unknown>,\n signal?: AbortSignal,\n) => Promise<ReadableStream<Uint8Array>>;\n\n/**\n * Invokes a serving endpoint using the SDK's high-level query API.\n * Returns a typed QueryEndpointResponse.\n */\nexport async function invoke(\n client: WorkspaceClient,\n endpointName: string,\n body: Record<string, unknown>,\n): Promise<serving.QueryEndpointResponse> {\n // Strip `stream` from the body — the connector controls this\n const { stream: _stream, ...cleanBody } = body;\n\n logger.debug(\"Invoking endpoint %s\", endpointName);\n\n return client.servingEndpoints.query({\n name: endpointName,\n ...cleanBody,\n } as serving.QueryEndpointInput);\n}\n\n/**\n * POSTs `body` as JSON to an arbitrary workspace API path and returns the raw\n * SSE byte stream. No parsing is performed — bytes are passed through as-is.\n *\n * Uses the SDK's low-level `apiClient.request({ raw: true })` so callers\n * inherit URL resolution, the SDK credential chain (PAT/OAuth/OIDC), and\n * any future retries/telemetry baked into the SDK transport.\n *\n * When `signal` is provided it is bridged to the SDK's `Context` /\n * `CancellationToken` so aborts cancel the outbound HTTP request.\n *\n * @internal\n *\n * Not part of the public AppKit surface. `path` is passed through to the\n * SDK without any allowlist — exposing this to user-controlled input would\n * turn it into workspace-credentialled SSRF (CWE-918). Internal callers\n * must hard-code the path (or build it from a closed enum). New callers\n * inside the package: keep this constraint, and do not re-export from\n * `beta.ts` or any other entry point.\n */\nexport async function streamPath(\n client: ApiClientLike,\n path: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n): Promise<ReadableStream<Uint8Array>> {\n logger.debug(\"Streaming from path %s\", path);\n\n const context = signal\n ? new Context({\n cancellationToken: cancellationTokenFromAbortSignal(signal),\n })\n : undefined;\n\n const response = (await client.apiClient.request(\n {\n path,\n method: \"POST\",\n headers: new Headers({\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n }),\n payload: body,\n raw: true,\n },\n context,\n )) as { contents: ReadableStream<Uint8Array> | null };\n\n if (!response.contents) {\n throw new Error(\"Response body is null — streaming not supported\");\n }\n\n return response.contents;\n}\n\n/**\n * Returns the raw SSE byte stream from a serving endpoint. Thin wrapper over\n * {@link streamPath} that handles serving-specific URL encoding and forces\n * `stream: true` in the payload.\n */\nexport async function stream(\n client: WorkspaceClient,\n endpointName: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n): Promise<ReadableStream<Uint8Array>> {\n const { stream: _stream, ...cleanBody } = body;\n return streamPath(\n client as unknown as ApiClientLike,\n `/serving-endpoints/${encodeURIComponent(endpointName)}/invocations`,\n { ...cleanBody, stream: true },\n signal,\n );\n}\n"],"mappings":";;;;;AAQA,MAAM,SAAS,aAAa,qBAAqB;;;;;;AAOjD,SAAS,iCACP,QACmB;CACnB,MAAM,4BAAY,IAAI,KAAiB;CACvC,MAAM,aAAa;AACjB,OAAK,MAAM,MAAM,UACf,KAAI;AACF,OAAI;UACE;;AAKZ,QAAO,iBAAiB,SAAS,MAAM,EAAE,SAAS,MAAM,CAAC;AAEzD,QAAO;EACL,IAAI,0BAA0B;AAC5B,UAAO,OAAO;;EAEhB,wBAAwB,UAAoC;AAC1D,aAAU,IAAI,SAAuB;AACrC,OAAI,OAAO,QACT,CAAK,UAAU;;EAGpB;;;;;;AAiCH,eAAsB,OACpB,QACA,cACA,MACwC;CAExC,MAAM,EAAE,QAAQ,SAAS,GAAG,cAAc;AAE1C,QAAO,MAAM,wBAAwB,aAAa;AAElD,QAAO,OAAO,iBAAiB,MAAM;EACnC,MAAM;EACN,GAAG;EACJ,CAA+B;;;;;;;;;;;;;;;;;;;;;;AAuBlC,eAAsB,WACpB,QACA,MACA,MACA,QACqC;AACrC,QAAO,MAAM,0BAA0B,KAAK;CAE5C,MAAM,UAAU,SACZ,IAAI,QAAQ,EACV,mBAAmB,iCAAiC,OAAO,EAC5D,CAAC,GACF;CAEJ,MAAM,WAAY,MAAM,OAAO,UAAU,QACvC;EACE;EACA,QAAQ;EACR,SAAS,IAAI,QAAQ;GACnB,gBAAgB;GAChB,QAAQ;GACT,CAAC;EACF,SAAS;EACT,KAAK;EACN,EACD,QACD;AAED,KAAI,CAAC,SAAS,SACZ,OAAM,IAAI,MAAM,kDAAkD;AAGpE,QAAO,SAAS;;;;;;;AAQlB,eAAsB,OACpB,QACA,cACA,MACA,QACqC;CACrC,MAAM,EAAE,QAAQ,SAAS,GAAG,cAAc;AAC1C,QAAO,WACL,QACA,sBAAsB,mBAAmB,aAAa,CAAC,eACvD;EAAE,GAAG;EAAW,QAAQ;EAAM,EAC9B,OACD"}
@@ -5,6 +5,8 @@ import { ConnectionError } from "../../errors/connection.js";
5
5
  import { ExecutionError } from "../../errors/execution.js";
6
6
  import { ValidationError } from "../../errors/validation.js";
7
7
  import "../../errors/index.js";
8
+ import { Context } from "../../workspace-client/legacy.js";
9
+ import "../../workspace-client/index.js";
8
10
  import { TelemetryManager } from "../../telemetry/telemetry-manager.js";
9
11
  import { SpanKind, SpanStatusCode } from "../../telemetry/index.js";
10
12
  import { ArrowStreamProcessor } from "../../stream/arrow-stream-processor.js";
@@ -12,7 +14,6 @@ import { buildEmptyArrowIPCBase64 } from "./arrow-schema.js";
12
14
  import { executeStatementDefaults } from "./defaults.js";
13
15
  import { WarehousePollBackoff } from "./warehouse-poll-backoff.js";
14
16
  import { WarehouseStatusEmitter } from "./warehouse-status-emitter.js";
15
- import { Context } from "@databricks/sdk-experimental";
16
17
 
17
18
  //#region src/connectors/sql-warehouse/client.ts
18
19
  const logger = createLogger("connectors:sql-warehouse");