@databricks/appkit 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appkit/package.js +1 -1
- package/dist/connectors/sql-warehouse/client.js +128 -38
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +4 -4
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +7 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +2 -2
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +9 -2
- package/dist/stream/stream-manager.js.map +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/dist/appkit/package.js
CHANGED
|
@@ -41,6 +41,8 @@ var SQLWarehouseConnector = class {
|
|
|
41
41
|
* {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}.
|
|
42
42
|
*/
|
|
43
43
|
_recentlyRunning = /* @__PURE__ */ new Map();
|
|
44
|
+
/** Per-warehouse readiness singleflight — one poll loop per cold start. */
|
|
45
|
+
_readinessInFlight = /* @__PURE__ */ new Map();
|
|
44
46
|
telemetry;
|
|
45
47
|
telemetryMetrics;
|
|
46
48
|
constructor(config) {
|
|
@@ -161,6 +163,7 @@ var SQLWarehouseConnector = class {
|
|
|
161
163
|
});
|
|
162
164
|
logger.error("Statement execution failed: %s", error instanceof Error ? error.message : String(error));
|
|
163
165
|
}
|
|
166
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
164
167
|
if (error instanceof AppKitError) throw error;
|
|
165
168
|
throw ExecutionError.statementFailed(error instanceof Error ? error.message : String(error));
|
|
166
169
|
} finally {
|
|
@@ -194,6 +197,14 @@ var SQLWarehouseConnector = class {
|
|
|
194
197
|
* immediately without any SDK round-trip or status emission. This keeps
|
|
195
198
|
* cache-hit analytics requests off the Databricks control plane.
|
|
196
199
|
*
|
|
200
|
+
* Concurrent callers for the same `warehouseId` share a single in-flight
|
|
201
|
+
* poll loop (singleflight): only the owner issues `get`/`start`; joiners
|
|
202
|
+
* receive broadcast `onStatus` updates and share the result promise.
|
|
203
|
+
* When every waiter disconnects before `warehouses.start`, the shared poll
|
|
204
|
+
* is aborted on the next microtask if still unclaimed (kills true orphans
|
|
205
|
+
* without a fixed grace window). After `start` is issued, the poll runs to
|
|
206
|
+
* completion even with no waiters.
|
|
207
|
+
*
|
|
197
208
|
* Behaviour by initial state:
|
|
198
209
|
* - `RUNNING`: emits one update, caches the observation, returns.
|
|
199
210
|
* - `STOPPED`: emits a synthetic `STARTING` update, calls
|
|
@@ -211,52 +222,51 @@ var SQLWarehouseConnector = class {
|
|
|
211
222
|
if (signal?.aborted) throw ExecutionError.canceled();
|
|
212
223
|
if (!warehouseId) throw ValidationError.missingField("warehouse_id");
|
|
213
224
|
if (this._isRecentlyRunning(warehouseId)) return;
|
|
214
|
-
|
|
225
|
+
const existing = this._readinessInFlight.get(warehouseId);
|
|
226
|
+
if (existing && !existing.sharedController.signal.aborted) return this._waitOnReadiness(existing, onStatus, signal, true);
|
|
227
|
+
const entry = this._spawnReadinessInFlight(workspaceClient, warehouseId, {
|
|
228
|
+
timeoutMs,
|
|
229
|
+
autoStart
|
|
230
|
+
});
|
|
231
|
+
this._readinessInFlight.set(warehouseId, entry);
|
|
232
|
+
return this._waitOnReadiness(entry, onStatus, signal, false);
|
|
233
|
+
}
|
|
234
|
+
_waitOnReadiness(entry, onStatus, signal, join) {
|
|
235
|
+
if (signal?.aborted) throw ExecutionError.canceled();
|
|
236
|
+
if (join) entry.refCount++;
|
|
237
|
+
entry.subscribers.add(onStatus);
|
|
238
|
+
if (entry.lastUpdate) onStatus(entry.lastUpdate);
|
|
239
|
+
return this._joinWarehouseReadiness(entry, signal);
|
|
240
|
+
}
|
|
241
|
+
_spawnReadinessInFlight(workspaceClient, warehouseId, opts) {
|
|
242
|
+
const sharedController = new AbortController();
|
|
243
|
+
const entry = {
|
|
244
|
+
refCount: 1,
|
|
245
|
+
sharedController,
|
|
246
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
247
|
+
lastUpdate: null,
|
|
248
|
+
warehouseStartIssued: false,
|
|
249
|
+
promise: Promise.resolve()
|
|
250
|
+
};
|
|
251
|
+
entry.promise = this.telemetry.startActiveSpan("sql.warehouseReady", {
|
|
215
252
|
kind: SpanKind.CLIENT,
|
|
216
253
|
attributes: {
|
|
217
254
|
"db.system": "databricks",
|
|
218
255
|
"db.warehouse_id": warehouseId,
|
|
219
|
-
"db.warehouse.startup_timeout_ms": timeoutMs
|
|
256
|
+
"db.warehouse.startup_timeout_ms": opts.timeoutMs
|
|
220
257
|
}
|
|
221
258
|
}, async (span) => {
|
|
222
|
-
const startTime = Date.now();
|
|
223
|
-
const emitter = new WarehouseStatusEmitter(span, startTime, onStatus);
|
|
224
|
-
let didStart = false;
|
|
225
|
-
const backoff = new WarehousePollBackoff();
|
|
226
259
|
try {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
case "RUNNING":
|
|
235
|
-
emitter.emit(state, summary);
|
|
236
|
-
this._recentlyRunning.set(warehouseId, Date.now());
|
|
237
|
-
span.setAttribute("db.warehouse.attempts", emitter.attempt);
|
|
238
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
239
|
-
return;
|
|
240
|
-
case "DELETED":
|
|
241
|
-
case "DELETING": throw ConfigurationError.resourceNotFound("Warehouse ID", `The configured SQL warehouse is ${state}. Update DATABRICKS_WAREHOUSE_ID to point at an active warehouse.`);
|
|
242
|
-
case "STOPPED":
|
|
243
|
-
if (!autoStart) throw new ConfigurationError("The configured SQL warehouse is STOPPED and analytics auto-start is disabled. Start the warehouse manually or set analytics.autoStartWarehouse=true.");
|
|
244
|
-
if (!didStart) {
|
|
245
|
-
emitter.emit("STARTING", summary);
|
|
246
|
-
await workspaceClient.warehouses.start({ id: warehouseId }, this._createContext(signal));
|
|
247
|
-
didStart = true;
|
|
248
|
-
} else emitter.emit(state, summary);
|
|
249
|
-
break;
|
|
250
|
-
case "STARTING":
|
|
251
|
-
case "STOPPING":
|
|
252
|
-
emitter.emit(state, summary);
|
|
253
|
-
break;
|
|
254
|
-
default: throw ExecutionError.unknownState(String(state ?? "unknown"));
|
|
260
|
+
await this._pollUntilWarehouseRunning(workspaceClient, warehouseId, {
|
|
261
|
+
onStatus: (update) => this._broadcastReadiness(entry, update),
|
|
262
|
+
signal: sharedController.signal,
|
|
263
|
+
timeoutMs: opts.timeoutMs,
|
|
264
|
+
autoStart: opts.autoStart,
|
|
265
|
+
onWarehouseStartIssued: () => {
|
|
266
|
+
entry.warehouseStartIssued = true;
|
|
255
267
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
await this._sleepRespectingAbort(sleepMs, signal);
|
|
259
|
-
}
|
|
268
|
+
}, span);
|
|
269
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
260
270
|
} catch (error) {
|
|
261
271
|
this._throwSanitizedReadinessError(error, warehouseId, span);
|
|
262
272
|
} finally {
|
|
@@ -265,7 +275,86 @@ var SQLWarehouseConnector = class {
|
|
|
265
275
|
}, {
|
|
266
276
|
name: this.name,
|
|
267
277
|
includePrefix: true
|
|
278
|
+
}).finally(() => {
|
|
279
|
+
if (this._readinessInFlight.get(warehouseId) === entry) this._readinessInFlight.delete(warehouseId);
|
|
268
280
|
});
|
|
281
|
+
entry.promise.catch(() => {});
|
|
282
|
+
return entry;
|
|
283
|
+
}
|
|
284
|
+
_broadcastReadiness(entry, update) {
|
|
285
|
+
entry.lastUpdate = update;
|
|
286
|
+
for (const subscriber of entry.subscribers) try {
|
|
287
|
+
subscriber(update);
|
|
288
|
+
} catch {}
|
|
289
|
+
}
|
|
290
|
+
/** Ref-counted await; caller abort rejects locally. */
|
|
291
|
+
_joinWarehouseReadiness(entry, callerSignal) {
|
|
292
|
+
if (!callerSignal) return entry.promise;
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
let done = false;
|
|
295
|
+
const finish = (outcome, error) => {
|
|
296
|
+
if (done) return;
|
|
297
|
+
done = true;
|
|
298
|
+
callerSignal.removeEventListener("abort", onAbort);
|
|
299
|
+
if (outcome === "ok") resolve();
|
|
300
|
+
else if (outcome === "abort") reject(ExecutionError.canceled());
|
|
301
|
+
else reject(error);
|
|
302
|
+
};
|
|
303
|
+
const onAbort = () => {
|
|
304
|
+
this._releaseReadinessWaiter(entry);
|
|
305
|
+
finish("abort");
|
|
306
|
+
};
|
|
307
|
+
callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
308
|
+
entry.promise.then(() => finish("ok"), (error) => finish("err", error));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
_releaseReadinessWaiter(entry) {
|
|
312
|
+
if (entry.refCount > 0) entry.refCount--;
|
|
313
|
+
if (entry.refCount > 0 || entry.sharedController.signal.aborted) return;
|
|
314
|
+
if (entry.warehouseStartIssued) return;
|
|
315
|
+
queueMicrotask(() => {
|
|
316
|
+
if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) entry.sharedController.abort("all warehouse readiness waiters aborted");
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async _pollUntilWarehouseRunning(workspaceClient, warehouseId, opts, span) {
|
|
320
|
+
const { onStatus, signal, timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS, autoStart = true, onWarehouseStartIssued } = opts;
|
|
321
|
+
const startTime = Date.now();
|
|
322
|
+
const emitter = new WarehouseStatusEmitter(span, startTime, onStatus);
|
|
323
|
+
let didStart = false;
|
|
324
|
+
const backoff = new WarehousePollBackoff();
|
|
325
|
+
while (true) {
|
|
326
|
+
if (signal?.aborted) throw ExecutionError.canceled();
|
|
327
|
+
if (Date.now() - startTime > timeoutMs) throw ExecutionError.statementFailed(`SQL warehouse did not reach RUNNING within ${timeoutMs}ms`);
|
|
328
|
+
const info = await workspaceClient.warehouses.get({ id: warehouseId }, this._createContext(signal));
|
|
329
|
+
const state = info?.state;
|
|
330
|
+
const summary = info?.health?.summary;
|
|
331
|
+
switch (state) {
|
|
332
|
+
case "RUNNING":
|
|
333
|
+
emitter.emit(state, summary);
|
|
334
|
+
this._recentlyRunning.set(warehouseId, Date.now());
|
|
335
|
+
span.setAttribute("db.warehouse.attempts", emitter.attempt);
|
|
336
|
+
return;
|
|
337
|
+
case "DELETED":
|
|
338
|
+
case "DELETING": throw ConfigurationError.resourceNotFound("Warehouse ID", `The configured SQL warehouse is ${state}. Update DATABRICKS_WAREHOUSE_ID to point at an active warehouse.`);
|
|
339
|
+
case "STOPPED":
|
|
340
|
+
if (!autoStart) throw new ConfigurationError("The configured SQL warehouse is STOPPED and analytics auto-start is disabled. Start the warehouse manually or set analytics.autoStartWarehouse=true.");
|
|
341
|
+
if (!didStart) {
|
|
342
|
+
emitter.emit("STARTING", summary);
|
|
343
|
+
onWarehouseStartIssued?.();
|
|
344
|
+
await workspaceClient.warehouses.start({ id: warehouseId }, this._createContext(signal));
|
|
345
|
+
didStart = true;
|
|
346
|
+
} else emitter.emit(state, summary);
|
|
347
|
+
break;
|
|
348
|
+
case "STARTING":
|
|
349
|
+
case "STOPPING":
|
|
350
|
+
emitter.emit(state, summary);
|
|
351
|
+
break;
|
|
352
|
+
default: throw ExecutionError.unknownState(String(state ?? "unknown"));
|
|
353
|
+
}
|
|
354
|
+
const sleepMs = backoff.next();
|
|
355
|
+
if (Date.now() + sleepMs - startTime >= timeoutMs) throw ExecutionError.statementFailed(`SQL warehouse did not reach RUNNING within ${timeoutMs}ms`);
|
|
356
|
+
await this._sleepRespectingAbort(sleepMs, signal);
|
|
357
|
+
}
|
|
269
358
|
}
|
|
270
359
|
/**
|
|
271
360
|
* `true` if this connector observed `warehouseId` in the RUNNING state
|
|
@@ -388,6 +477,7 @@ var SQLWarehouseConnector = class {
|
|
|
388
477
|
code: SpanStatusCode.ERROR,
|
|
389
478
|
message: error instanceof Error ? error.message : String(error)
|
|
390
479
|
});
|
|
480
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
391
481
|
if (error instanceof AppKitError) throw error;
|
|
392
482
|
throw ExecutionError.statementFailed(error instanceof Error ? error.message : String(error));
|
|
393
483
|
} finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/sql-warehouse/client.ts"],"sourcesContent":["import {\n Context,\n type sql,\n type WorkspaceClient,\n} from \"@databricks/sdk-experimental\";\nimport type { TelemetryOptions } from \"shared\";\nimport {\n AppKitError,\n ConfigurationError,\n ConnectionError,\n ExecutionError,\n ValidationError,\n} from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { ArrowStreamProcessor } from \"../../stream/arrow-stream-processor\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport { executeStatementDefaults } from \"./defaults\";\nimport { WarehousePollBackoff } from \"./warehouse-poll-backoff\";\nimport { WarehouseStatusEmitter } from \"./warehouse-status-emitter\";\n\nconst logger = createLogger(\"connectors:sql-warehouse\");\n\ninterface SQLWarehouseConfig {\n timeout?: number;\n telemetry?: TelemetryOptions;\n}\n\n/**\n * Default ceiling for how long {@link SQLWarehouseConnector.ensureWarehouseRunning}\n * will wait for a warehouse to reach the RUNNING state before giving up.\n *\n * Five minutes covers a cold-start of a classic warehouse on most workspaces;\n * serverless typically reaches RUNNING within ~30s.\n */\nexport const DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Window during which a recent `RUNNING` observation lets subsequent calls\n * to {@link SQLWarehouseConnector.ensureWarehouseRunning} short-circuit\n * without making any SDK calls. Sized to roughly the upper bound of how\n * long Databricks keeps a warehouse \"stickily\" available between requests\n * — past that, we re-verify.\n */\nconst WAREHOUSE_RUNNING_CACHE_TTL_MS = 30_000;\n\n/**\n * A single observation of the warehouse state, emitted by\n * {@link SQLWarehouseConnector.ensureWarehouseRunning} so callers can stream\n * progress to clients (e.g. over SSE).\n *\n * Note: `health.summary` from the SDK is intentionally NOT exposed on this\n * type. It's free-form operator-oriented diagnostic text (cluster IDs,\n * capacity-failure reasons, internal RPC errors) that must not reach end\n * users. The raw value stays in the OTel span attributes for server-side\n * debugging only.\n */\nexport interface WarehouseStatusUpdate {\n /** Current state from the SDK (RUNNING | STARTING | STOPPED | STOPPING | DELETED | DELETING). */\n state: sql.State;\n /** Milliseconds elapsed since `ensureWarehouseRunning` was called. */\n elapsedMs: number;\n /** 1-based attempt counter — useful for tests and telemetry. */\n attempt: number;\n}\n\n/** Options for {@link SQLWarehouseConnector.ensureWarehouseRunning}. */\ninterface EnsureWarehouseRunningOptions {\n /** Invoked every time the warehouse state changes during the wait. */\n onStatus: (update: WarehouseStatusUpdate) => void;\n /** Aborts the wait. The connector treats abort as `ExecutionError.canceled()`. */\n signal?: AbortSignal;\n /** Hard ceiling on the total wait. Defaults to {@link DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS}. */\n timeoutMs?: number;\n /**\n * When `true` (default), a `STOPPED` warehouse is auto-started.\n * When `false`, `STOPPED` surfaces as `ConfigurationError` immediately —\n * useful for cost-controlled deployments that don't want any caller to\n * trigger billable warehouse starts.\n */\n autoStart?: boolean;\n}\n\nexport class SQLWarehouseConnector {\n private readonly name = \"sql-warehouse\";\n\n private config: SQLWarehouseConfig;\n\n // Lazy-initialized: only created when Arrow format is used\n private _arrowProcessor: ArrowStreamProcessor | null = null;\n\n /**\n * Per-warehouse cache of the last RUNNING observation timestamp. Used by\n * {@link ensureWarehouseRunning} to short-circuit warm-path callers; see\n * {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}.\n */\n private _recentlyRunning = new Map<string, number>();\n // telemetry\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n queryCount: Counter;\n queryDuration: Histogram;\n };\n\n constructor(config: SQLWarehouseConfig) {\n this.config = config;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n queryCount: this.telemetry.getMeter().createCounter(\"query.count\", {\n description: \"Total number of queries executed\",\n unit: \"1\",\n }),\n queryDuration: this.telemetry\n .getMeter()\n .createHistogram(\"query.duration\", {\n description: \"Duration of queries executed\",\n unit: \"ms\",\n }),\n };\n }\n\n /**\n * Lazily initializes and returns the ArrowStreamProcessor.\n * Only created on first Arrow format query to avoid unnecessary allocation.\n */\n private get arrowProcessor(): ArrowStreamProcessor {\n if (!this._arrowProcessor) {\n this._arrowProcessor = new ArrowStreamProcessor({\n timeout: this.config.timeout || executeStatementDefaults.timeout,\n maxConcurrentDownloads:\n ArrowStreamProcessor.DEFAULT_MAX_CONCURRENT_DOWNLOADS,\n retries: ArrowStreamProcessor.DEFAULT_RETRIES,\n });\n }\n return this._arrowProcessor;\n }\n\n async executeStatement(\n workspaceClient: WorkspaceClient,\n input: sql.ExecuteStatementRequest,\n signal?: AbortSignal,\n ) {\n const startTime = Date.now();\n let success = false;\n\n // if signal is aborted, throw an error\n if (signal?.aborted) {\n throw ExecutionError.canceled();\n }\n\n return this.telemetry.startActiveSpan(\n \"sql.query\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": input.warehouse_id || \"\",\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n \"db.has_parameters\": !!input.parameters,\n },\n },\n async (span: Span) => {\n let abortHandler: (() => void) | undefined;\n let isAborted = false;\n\n if (signal) {\n abortHandler = () => {\n // abort span if not recording\n if (!span.isRecording()) return;\n isAborted = true;\n span.setAttribute(\"cancelled\", true);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: \"Query cancelled by client\",\n });\n span.end();\n };\n signal.addEventListener(\"abort\", abortHandler, { once: true });\n }\n\n try {\n // validate required fields\n if (!input.statement) {\n throw ValidationError.missingField(\"statement\");\n }\n\n if (!input.warehouse_id) {\n throw ValidationError.missingField(\"warehouse_id\");\n }\n\n const body: sql.ExecuteStatementRequest = {\n statement: input.statement,\n parameters: input.parameters,\n warehouse_id: input.warehouse_id,\n catalog: input.catalog,\n schema: input.schema,\n wait_timeout:\n input.wait_timeout || executeStatementDefaults.wait_timeout,\n disposition:\n input.disposition || executeStatementDefaults.disposition,\n format: input.format || executeStatementDefaults.format,\n byte_limit: input.byte_limit,\n row_limit: input.row_limit,\n on_wait_timeout:\n input.on_wait_timeout || executeStatementDefaults.on_wait_timeout,\n };\n\n span.addEvent(\"statement.submitting\", {\n \"db.warehouse_id\": input.warehouse_id,\n });\n\n const response =\n await workspaceClient.statementExecution.executeStatement(\n body,\n this._createContext(signal),\n );\n\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n const status = response.status;\n const statementId = response.statement_id as string;\n\n span.setAttribute(\"db.statement_id\", statementId);\n span.addEvent(\"statement.submitted\", {\n \"db.statement_id\": response.statement_id,\n \"db.status\": status?.state,\n });\n\n let result:\n | sql.StatementResponse\n | { result: { statement_id: string; status: sql.StatementStatus } };\n\n switch (status?.state) {\n case \"RUNNING\":\n case \"PENDING\":\n span.addEvent(\"statement.polling_started\", {\n \"db.status\": response.status?.state,\n });\n result = await this._pollForStatementResult(\n workspaceClient,\n statementId,\n this.config.timeout,\n signal,\n );\n break;\n case \"SUCCEEDED\":\n result = this._transformDataArray(response);\n break;\n case \"FAILED\":\n throw ExecutionError.statementFailed(status.error?.message);\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n const resultData = result.result as any;\n const rowCount =\n resultData?.data?.length ?? resultData?.data_array?.length ?? 0;\n\n if (rowCount > 0) {\n span.setAttribute(\"db.result.row_count\", rowCount);\n }\n\n const duration = Date.now() - startTime;\n logger.event()?.setContext(\"sql-warehouse\", {\n warehouse_id: input.warehouse_id,\n rows_returned: rowCount,\n query_duration_ms: duration,\n });\n\n success = true;\n // only set success status if not aborted\n if (!isAborted) {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n return result;\n } catch (error) {\n // only record error if not already handled by abort\n if (!isAborted) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n logger.error(\n \"Statement execution failed: %s\",\n error instanceof Error ? error.message : String(error),\n );\n }\n\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n } finally {\n // remove abort handler\n if (abortHandler && signal) {\n signal.removeEventListener(\"abort\", abortHandler);\n }\n\n const duration = Date.now() - startTime;\n\n // end span if not already ended by abort handler\n if (!isAborted) {\n span.end();\n }\n\n const attributes = {\n \"db.warehouse_id\": input.warehouse_id,\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n success: success.toString(),\n };\n\n this.telemetryMetrics.queryCount.add(1, attributes);\n this.telemetryMetrics.queryDuration.record(duration, attributes);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait until the SQL warehouse is in the `RUNNING` state, auto-starting it\n * if currently `STOPPED`. Emits a {@link WarehouseStatusUpdate} whenever\n * the observed state changes, so callers can surface progress (e.g. over\n * SSE) instead of letting the UI freeze on a cold warehouse. Equal\n * successive observations are de-duplicated.\n *\n * Fast path: if this connector recently observed the warehouse RUNNING\n * (within {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}), the call returns\n * immediately without any SDK round-trip or status emission. This keeps\n * cache-hit analytics requests off the Databricks control plane.\n *\n * Behaviour by initial state:\n * - `RUNNING`: emits one update, caches the observation, returns.\n * - `STOPPED`: emits a synthetic `STARTING` update, calls\n * `workspaceClient.warehouses.start`, then polls until `RUNNING`.\n * When `autoStart: false`, throws `ConfigurationError` instead.\n * - `STARTING` / `STOPPING`: polls until `RUNNING`.\n * - `DELETED` / `DELETING`: throws `ConfigurationError.resourceNotFound`.\n *\n * Aborts and timeouts surface as `ExecutionError`. The poll loop uses\n * exponential backoff with ±15% jitter (1s → 30s cap) and runs inside a\n * `sql.warehouseReady` telemetry span.\n */\n async ensureWarehouseRunning(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: EnsureWarehouseRunningOptions,\n ): Promise<void> {\n const {\n onStatus,\n signal,\n timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n autoStart = true,\n } = opts;\n\n if (signal?.aborted) throw ExecutionError.canceled();\n if (!warehouseId) throw ValidationError.missingField(\"warehouse_id\");\n if (this._isRecentlyRunning(warehouseId)) return;\n\n return this.telemetry.startActiveSpan(\n \"sql.warehouseReady\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": warehouseId,\n \"db.warehouse.startup_timeout_ms\": timeoutMs,\n },\n },\n async (span: Span) => {\n const startTime = Date.now();\n const emitter = new WarehouseStatusEmitter(span, startTime, onStatus);\n let didStart = false;\n const backoff = new WarehousePollBackoff();\n\n try {\n while (true) {\n if (signal?.aborted) throw ExecutionError.canceled();\n if (Date.now() - startTime > timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n\n const info = await workspaceClient.warehouses.get(\n { id: warehouseId },\n this._createContext(signal),\n );\n const state = info?.state;\n const summary = info?.health?.summary;\n\n switch (state) {\n case \"RUNNING\":\n emitter.emit(state, summary);\n this._recentlyRunning.set(warehouseId, Date.now());\n span.setAttribute(\"db.warehouse.attempts\", emitter.attempt);\n span.setStatus({ code: SpanStatusCode.OK });\n return;\n case \"DELETED\":\n case \"DELETING\":\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n `The configured SQL warehouse is ${state}. Update DATABRICKS_WAREHOUSE_ID to point at an active warehouse.`,\n );\n case \"STOPPED\":\n if (!autoStart) {\n // The warehouse exists, it's just in the wrong state and\n // policy forbids auto-starting it — use the base\n // ConfigurationError directly so the message isn't\n // prefixed with \"not found\".\n throw new ConfigurationError(\n \"The configured SQL warehouse is STOPPED and analytics auto-start is disabled. Start the warehouse manually or set analytics.autoStartWarehouse=true.\",\n );\n }\n if (!didStart) {\n emitter.emit(\"STARTING\", summary);\n await workspaceClient.warehouses.start(\n { id: warehouseId },\n this._createContext(signal),\n );\n didStart = true;\n } else {\n // Warehouse fell back to STOPPED after our start attempt —\n // surface the state and keep polling.\n emitter.emit(state, summary);\n }\n break;\n case \"STARTING\":\n case \"STOPPING\":\n emitter.emit(state, summary);\n break;\n default:\n throw ExecutionError.unknownState(String(state ?? \"unknown\"));\n }\n\n // Preempt the next timeout check: if sleeping the full backoff\n // would push us past `timeoutMs`, throw now rather than waste\n // up to ~30s sleeping past the deadline (`backoff.next()` caps\n // at WAREHOUSE_POLL_MAX_MS).\n const sleepMs = backoff.next();\n if (Date.now() + sleepMs - startTime >= timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n await this._sleepRespectingAbort(sleepMs, signal);\n }\n } catch (error) {\n this._throwSanitizedReadinessError(error, warehouseId, span);\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * `true` if this connector observed `warehouseId` in the RUNNING state\n * within the recent-cache TTL.\n */\n private _isRecentlyRunning(warehouseId: string): boolean {\n const observedAt = this._recentlyRunning.get(warehouseId);\n return (\n observedAt !== undefined &&\n Date.now() - observedAt < WAREHOUSE_RUNNING_CACHE_TTL_MS\n );\n }\n\n /**\n * Final landing for any error thrown by the readiness poll loop. Records\n * the original on the span and (at debug level) on the logger, then\n * rethrows either the structured AppKitError unchanged or a curated\n * ExecutionError — never the raw SDK message, which can contain operator\n * internals.\n *\n * The `logger.debug` covers environments where OTel isn't configured\n * (common in dev) so the raw error isn't lost just because no span\n * exporter is hooked up.\n */\n private _throwSanitizedReadinessError(\n error: unknown,\n warehouseId: string,\n span: Span,\n ): never {\n if (error instanceof AppKitError) {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.code });\n throw error;\n }\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n logger.debug(\n \"Warehouse readiness check raw error for %s: %O\",\n warehouseId,\n error,\n );\n const wrapped = ExecutionError.statementFailed(\n \"Warehouse readiness check failed\",\n );\n span.setStatus({ code: SpanStatusCode.ERROR, message: wrapped.code });\n throw wrapped;\n }\n\n /**\n * Sleep for `ms` milliseconds, but resolve early (and reject with\n * `ExecutionError.canceled()`) if `signal` aborts mid-sleep. Used by the\n * warehouse readiness loop so that client disconnects don't keep the loop\n * polling for the full interval.\n */\n private _sleepRespectingAbort(\n ms: number,\n signal?: AbortSignal,\n ): Promise<void> {\n if (signal?.aborted) return Promise.reject(ExecutionError.canceled());\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(ExecutionError.canceled());\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n private async _pollForStatementResult(\n workspaceClient: WorkspaceClient,\n statementId: string,\n timeout = executeStatementDefaults.timeout,\n signal?: AbortSignal,\n ) {\n return this.telemetry.startActiveSpan(\n \"sql.poll\",\n {\n attributes: {\n \"db.statement_id\": statementId,\n \"db.polling.timeout\": timeout,\n },\n },\n async (span: Span) => {\n try {\n const startTime = Date.now();\n let delay = 1000;\n const maxDelayBetweenPolls = 5000; // max 5 seconds between polls\n let pollCount = 0;\n\n while (true) {\n pollCount++;\n span.setAttribute(\"db.polling.current_attempt\", pollCount);\n\n // check if timeout exceeded\n const elapsedTime = Date.now() - startTime;\n if (elapsedTime > timeout) {\n const error = ExecutionError.statementFailed(\n `Polling timeout exceeded after ${timeout}ms (elapsed: ${elapsedTime}ms)`,\n );\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n if (signal?.aborted) {\n const error = ExecutionError.canceled();\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n span.addEvent(\"polling.attempt\", {\n \"poll.attempt\": pollCount,\n \"poll.delay_ms\": delay,\n \"poll.elapsed_ms\": elapsedTime,\n });\n\n const response =\n await workspaceClient.statementExecution.getStatement(\n {\n statement_id: statementId,\n },\n this._createContext(signal),\n );\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n\n const status = response.status;\n\n span.addEvent(\"polling.status_check\", {\n \"db.status\": status?.state,\n \"poll.attempt\": pollCount,\n });\n\n switch (status?.state) {\n case \"PENDING\":\n case \"RUNNING\":\n // continue polling\n break;\n case \"SUCCEEDED\":\n span.setAttribute(\"db.polling.attempts\", pollCount);\n span.setAttribute(\"db.polling.total_duration_ms\", elapsedTime);\n span.addEvent(\"polling.completed\", {\n \"poll.attempts\": pollCount,\n \"poll.duration_ms\": elapsedTime,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n return this._transformDataArray(response);\n case \"FAILED\":\n throw ExecutionError.statementFailed(status.error?.message);\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n // continue polling after delay\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * 2, maxDelayBetweenPolls);\n }\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n // error logging is handled by executeStatement's catch block (gated on isAborted)\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n private _transformDataArray(response: sql.StatementResponse) {\n if (response.manifest?.format === \"ARROW_STREAM\") {\n return this.updateWithArrowStatus(response);\n }\n\n if (!response.result?.data_array || !response.manifest?.schema?.columns) {\n return response;\n }\n\n const columns = response.manifest.schema.columns;\n\n const transformedData = response.result.data_array.map((row) => {\n const obj: Record<string, unknown> = {};\n row.forEach((value, index) => {\n const column = columns[index];\n const columnName = column?.name || `column_${index}`;\n\n // attempt to parse JSON strings for string columns\n if (\n column?.type_name === \"STRING\" &&\n typeof value === \"string\" &&\n value &&\n (value[0] === \"{\" || value[0] === \"[\")\n ) {\n try {\n obj[columnName] = JSON.parse(value);\n } catch {\n // if parsing fails, keep as string\n obj[columnName] = value;\n }\n } else {\n obj[columnName] = value;\n }\n });\n return obj;\n });\n\n // remove data_array\n const { data_array: _data_array, ...restResult } = response.result;\n return {\n ...response,\n result: {\n ...restResult,\n data: transformedData,\n },\n };\n }\n\n private updateWithArrowStatus(response: sql.StatementResponse): {\n result: { statement_id: string; status: sql.StatementStatus };\n } {\n return {\n result: {\n statement_id: response.statement_id as string,\n status: {\n state: response.status?.state,\n error: response.status?.error,\n } as sql.StatementStatus,\n },\n };\n }\n\n async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.arrowProcessor.processChunks>> {\n const startTime = Date.now();\n\n return this.telemetry.startActiveSpan(\n \"arrow.getData\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"arrow.job_id\": jobId,\n },\n },\n async (span: Span) => {\n try {\n const response =\n await workspaceClient.statementExecution.getStatement(\n { statement_id: jobId },\n this._createContext(signal),\n );\n\n const chunks = response.result?.external_links;\n const schema = response.manifest?.schema;\n\n if (!chunks || !schema) {\n throw ExecutionError.missingData(\"chunks or schema\");\n }\n\n span.setAttribute(\"arrow.chunk_count\", chunks.length);\n\n const result = await this.arrowProcessor.processChunks(\n chunks,\n schema,\n signal,\n );\n\n span.setAttribute(\"arrow.data_size_bytes\", result.data.length);\n span.setStatus({ code: SpanStatusCode.OK });\n\n const duration = Date.now() - startTime;\n this.telemetryMetrics.queryDuration.record(duration, {\n operation: \"arrow.getData\",\n status: \"success\",\n });\n\n logger.event()?.setContext(\"sql-warehouse\", {\n arrow_data_size_bytes: result.data.length,\n arrow_job_id: jobId,\n });\n\n return result;\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : \"Unknown error\",\n });\n span.recordException(error as Error);\n\n const duration = Date.now() - startTime;\n this.telemetryMetrics.queryDuration.record(duration, {\n operation: \"arrow.getData\",\n status: \"error\",\n });\n\n logger.error(\"Failed Arrow job: %s %O\", jobId, error);\n\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n }\n },\n );\n }\n\n // create context for cancellation token\n private _createContext(signal?: AbortSignal) {\n return new Context({\n cancellationToken: {\n isCancellationRequested: signal?.aborted ?? false,\n onCancellationRequested: (cb: () => void) => {\n signal?.addEventListener(\"abort\", cb, { once: true });\n },\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../../src/connectors/sql-warehouse/client.ts"],"sourcesContent":["import {\n Context,\n type sql,\n type WorkspaceClient,\n} from \"@databricks/sdk-experimental\";\nimport type { TelemetryOptions } from \"shared\";\nimport {\n AppKitError,\n ConfigurationError,\n ConnectionError,\n ExecutionError,\n ValidationError,\n} from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { ArrowStreamProcessor } from \"../../stream/arrow-stream-processor\";\nimport type { TelemetryProvider } from \"../../telemetry\";\nimport {\n type Counter,\n type Histogram,\n type Span,\n SpanKind,\n SpanStatusCode,\n TelemetryManager,\n} from \"../../telemetry\";\nimport { executeStatementDefaults } from \"./defaults\";\nimport { WarehousePollBackoff } from \"./warehouse-poll-backoff\";\nimport { WarehouseStatusEmitter } from \"./warehouse-status-emitter\";\n\nconst logger = createLogger(\"connectors:sql-warehouse\");\n\ninterface SQLWarehouseConfig {\n timeout?: number;\n telemetry?: TelemetryOptions;\n}\n\n/**\n * Default ceiling for how long {@link SQLWarehouseConnector.ensureWarehouseRunning}\n * will wait for a warehouse to reach the RUNNING state before giving up.\n *\n * Five minutes covers a cold-start of a classic warehouse on most workspaces;\n * serverless typically reaches RUNNING within ~30s.\n */\nexport const DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS = 5 * 60 * 1000;\n\n/**\n * Window during which a recent `RUNNING` observation lets subsequent calls\n * to {@link SQLWarehouseConnector.ensureWarehouseRunning} short-circuit\n * without making any SDK calls. Sized to roughly the upper bound of how\n * long Databricks keeps a warehouse \"stickily\" available between requests\n * — past that, we re-verify.\n */\nconst WAREHOUSE_RUNNING_CACHE_TTL_MS = 30_000;\n\n/**\n * A single observation of the warehouse state, emitted by\n * {@link SQLWarehouseConnector.ensureWarehouseRunning} so callers can stream\n * progress to clients (e.g. over SSE).\n *\n * Note: `health.summary` from the SDK is intentionally NOT exposed on this\n * type. It's free-form operator-oriented diagnostic text (cluster IDs,\n * capacity-failure reasons, internal RPC errors) that must not reach end\n * users. The raw value stays in the OTel span attributes for server-side\n * debugging only.\n */\nexport interface WarehouseStatusUpdate {\n /** Current state from the SDK (RUNNING | STARTING | STOPPED | STOPPING | DELETED | DELETING). */\n state: sql.State;\n /** Milliseconds elapsed since `ensureWarehouseRunning` was called. */\n elapsedMs: number;\n /** 1-based attempt counter — useful for tests and telemetry. */\n attempt: number;\n}\n\n/** Options for {@link SQLWarehouseConnector.ensureWarehouseRunning}. */\ninterface EnsureWarehouseRunningOptions {\n /** Invoked every time the warehouse state changes during the wait. */\n onStatus: (update: WarehouseStatusUpdate) => void;\n /** Aborts the wait. The connector treats abort as `ExecutionError.canceled()`. */\n signal?: AbortSignal;\n /** Hard ceiling on the total wait. Defaults to {@link DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS}. */\n timeoutMs?: number;\n /**\n * When `true` (default), a `STOPPED` warehouse is auto-started.\n * When `false`, `STOPPED` surfaces as `ConfigurationError` immediately —\n * useful for cost-controlled deployments that don't want any caller to\n * trigger billable warehouse starts.\n */\n autoStart?: boolean;\n /** @internal Fired once when `warehouses.start` is issued for this poll. */\n onWarehouseStartIssued?: () => void;\n}\n\n/** In-flight warehouse readiness work shared across concurrent callers. */\ninterface WarehouseReadinessInFlight {\n promise: Promise<void>;\n refCount: number;\n sharedController: AbortController;\n subscribers: Set<(update: WarehouseStatusUpdate) => void>;\n /** Last emitted update; replayed to late joiners. */\n lastUpdate: WarehouseStatusUpdate | null;\n /** `true` once this poll has called `warehouses.start`. */\n warehouseStartIssued: boolean;\n}\n\nexport class SQLWarehouseConnector {\n private readonly name = \"sql-warehouse\";\n\n private config: SQLWarehouseConfig;\n\n // Lazy-initialized: only created when Arrow format is used\n private _arrowProcessor: ArrowStreamProcessor | null = null;\n\n /**\n * Per-warehouse cache of the last RUNNING observation timestamp. Used by\n * {@link ensureWarehouseRunning} to short-circuit warm-path callers; see\n * {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}.\n */\n private _recentlyRunning = new Map<string, number>();\n /** Per-warehouse readiness singleflight — one poll loop per cold start. */\n private _readinessInFlight = new Map<string, WarehouseReadinessInFlight>();\n // telemetry\n private readonly telemetry: TelemetryProvider;\n private readonly telemetryMetrics: {\n queryCount: Counter;\n queryDuration: Histogram;\n };\n\n constructor(config: SQLWarehouseConfig) {\n this.config = config;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n queryCount: this.telemetry.getMeter().createCounter(\"query.count\", {\n description: \"Total number of queries executed\",\n unit: \"1\",\n }),\n queryDuration: this.telemetry\n .getMeter()\n .createHistogram(\"query.duration\", {\n description: \"Duration of queries executed\",\n unit: \"ms\",\n }),\n };\n }\n\n /**\n * Lazily initializes and returns the ArrowStreamProcessor.\n * Only created on first Arrow format query to avoid unnecessary allocation.\n */\n private get arrowProcessor(): ArrowStreamProcessor {\n if (!this._arrowProcessor) {\n this._arrowProcessor = new ArrowStreamProcessor({\n timeout: this.config.timeout || executeStatementDefaults.timeout,\n maxConcurrentDownloads:\n ArrowStreamProcessor.DEFAULT_MAX_CONCURRENT_DOWNLOADS,\n retries: ArrowStreamProcessor.DEFAULT_RETRIES,\n });\n }\n return this._arrowProcessor;\n }\n\n async executeStatement(\n workspaceClient: WorkspaceClient,\n input: sql.ExecuteStatementRequest,\n signal?: AbortSignal,\n ) {\n const startTime = Date.now();\n let success = false;\n\n // if signal is aborted, throw an error\n if (signal?.aborted) {\n throw ExecutionError.canceled();\n }\n\n return this.telemetry.startActiveSpan(\n \"sql.query\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": input.warehouse_id || \"\",\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n \"db.has_parameters\": !!input.parameters,\n },\n },\n async (span: Span) => {\n let abortHandler: (() => void) | undefined;\n let isAborted = false;\n\n if (signal) {\n abortHandler = () => {\n // abort span if not recording\n if (!span.isRecording()) return;\n isAborted = true;\n span.setAttribute(\"cancelled\", true);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: \"Query cancelled by client\",\n });\n span.end();\n };\n signal.addEventListener(\"abort\", abortHandler, { once: true });\n }\n\n try {\n // validate required fields\n if (!input.statement) {\n throw ValidationError.missingField(\"statement\");\n }\n\n if (!input.warehouse_id) {\n throw ValidationError.missingField(\"warehouse_id\");\n }\n\n const body: sql.ExecuteStatementRequest = {\n statement: input.statement,\n parameters: input.parameters,\n warehouse_id: input.warehouse_id,\n catalog: input.catalog,\n schema: input.schema,\n wait_timeout:\n input.wait_timeout || executeStatementDefaults.wait_timeout,\n disposition:\n input.disposition || executeStatementDefaults.disposition,\n format: input.format || executeStatementDefaults.format,\n byte_limit: input.byte_limit,\n row_limit: input.row_limit,\n on_wait_timeout:\n input.on_wait_timeout || executeStatementDefaults.on_wait_timeout,\n };\n\n span.addEvent(\"statement.submitting\", {\n \"db.warehouse_id\": input.warehouse_id,\n });\n\n const response =\n await workspaceClient.statementExecution.executeStatement(\n body,\n this._createContext(signal),\n );\n\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n const status = response.status;\n const statementId = response.statement_id as string;\n\n span.setAttribute(\"db.statement_id\", statementId);\n span.addEvent(\"statement.submitted\", {\n \"db.statement_id\": response.statement_id,\n \"db.status\": status?.state,\n });\n\n let result:\n | sql.StatementResponse\n | { result: { statement_id: string; status: sql.StatementStatus } };\n\n switch (status?.state) {\n case \"RUNNING\":\n case \"PENDING\":\n span.addEvent(\"statement.polling_started\", {\n \"db.status\": response.status?.state,\n });\n result = await this._pollForStatementResult(\n workspaceClient,\n statementId,\n this.config.timeout,\n signal,\n );\n break;\n case \"SUCCEEDED\":\n result = this._transformDataArray(response);\n break;\n case \"FAILED\":\n throw ExecutionError.statementFailed(status.error?.message);\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n const resultData = result.result as any;\n const rowCount =\n resultData?.data?.length ?? resultData?.data_array?.length ?? 0;\n\n if (rowCount > 0) {\n span.setAttribute(\"db.result.row_count\", rowCount);\n }\n\n const duration = Date.now() - startTime;\n logger.event()?.setContext(\"sql-warehouse\", {\n warehouse_id: input.warehouse_id,\n rows_returned: rowCount,\n query_duration_ms: duration,\n });\n\n success = true;\n // only set success status if not aborted\n if (!isAborted) {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n return result;\n } catch (error) {\n // only record error if not already handled by abort\n if (!isAborted) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n logger.error(\n \"Statement execution failed: %s\",\n error instanceof Error ? error.message : String(error),\n );\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n throw error;\n }\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n } finally {\n // remove abort handler\n if (abortHandler && signal) {\n signal.removeEventListener(\"abort\", abortHandler);\n }\n\n const duration = Date.now() - startTime;\n\n // end span if not already ended by abort handler\n if (!isAborted) {\n span.end();\n }\n\n const attributes = {\n \"db.warehouse_id\": input.warehouse_id,\n \"db.catalog\": input.catalog ?? \"\",\n \"db.schema\": input.schema ?? \"\",\n \"db.statement\": input.statement?.substring(0, 500) || \"\",\n success: success.toString(),\n };\n\n this.telemetryMetrics.queryCount.add(1, attributes);\n this.telemetryMetrics.queryDuration.record(duration, attributes);\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait until the SQL warehouse is in the `RUNNING` state, auto-starting it\n * if currently `STOPPED`. Emits a {@link WarehouseStatusUpdate} whenever\n * the observed state changes, so callers can surface progress (e.g. over\n * SSE) instead of letting the UI freeze on a cold warehouse. Equal\n * successive observations are de-duplicated.\n *\n * Fast path: if this connector recently observed the warehouse RUNNING\n * (within {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}), the call returns\n * immediately without any SDK round-trip or status emission. This keeps\n * cache-hit analytics requests off the Databricks control plane.\n *\n * Concurrent callers for the same `warehouseId` share a single in-flight\n * poll loop (singleflight): only the owner issues `get`/`start`; joiners\n * receive broadcast `onStatus` updates and share the result promise.\n * When every waiter disconnects before `warehouses.start`, the shared poll\n * is aborted on the next microtask if still unclaimed (kills true orphans\n * without a fixed grace window). After `start` is issued, the poll runs to\n * completion even with no waiters.\n *\n * Behaviour by initial state:\n * - `RUNNING`: emits one update, caches the observation, returns.\n * - `STOPPED`: emits a synthetic `STARTING` update, calls\n * `workspaceClient.warehouses.start`, then polls until `RUNNING`.\n * When `autoStart: false`, throws `ConfigurationError` instead.\n * - `STARTING` / `STOPPING`: polls until `RUNNING`.\n * - `DELETED` / `DELETING`: throws `ConfigurationError.resourceNotFound`.\n *\n * Aborts and timeouts surface as `ExecutionError`. The poll loop uses\n * exponential backoff with ±15% jitter (1s → 30s cap) and runs inside a\n * `sql.warehouseReady` telemetry span.\n */\n async ensureWarehouseRunning(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: EnsureWarehouseRunningOptions,\n ): Promise<void> {\n const {\n onStatus,\n signal,\n timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n autoStart = true,\n } = opts;\n\n if (signal?.aborted) throw ExecutionError.canceled();\n if (!warehouseId) throw ValidationError.missingField(\"warehouse_id\");\n if (this._isRecentlyRunning(warehouseId)) return;\n\n const existing = this._readinessInFlight.get(warehouseId);\n if (existing && !existing.sharedController.signal.aborted) {\n return this._waitOnReadiness(existing, onStatus, signal, true);\n }\n\n const entry = this._spawnReadinessInFlight(workspaceClient, warehouseId, {\n timeoutMs,\n autoStart,\n });\n this._readinessInFlight.set(warehouseId, entry);\n return this._waitOnReadiness(entry, onStatus, signal, false);\n }\n\n private _waitOnReadiness(\n entry: WarehouseReadinessInFlight,\n onStatus: (update: WarehouseStatusUpdate) => void,\n signal: AbortSignal | undefined,\n join: boolean,\n ): Promise<void> {\n if (signal?.aborted) throw ExecutionError.canceled();\n if (join) entry.refCount++;\n entry.subscribers.add(onStatus);\n if (entry.lastUpdate) onStatus(entry.lastUpdate);\n return this._joinWarehouseReadiness(entry, signal);\n }\n\n private _spawnReadinessInFlight(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: { timeoutMs: number; autoStart: boolean },\n ): WarehouseReadinessInFlight {\n const sharedController = new AbortController();\n const entry: WarehouseReadinessInFlight = {\n refCount: 1,\n sharedController,\n subscribers: new Set(),\n lastUpdate: null,\n warehouseStartIssued: false,\n promise: Promise.resolve(),\n };\n\n entry.promise = this.telemetry\n .startActiveSpan(\n \"sql.warehouseReady\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"db.warehouse_id\": warehouseId,\n \"db.warehouse.startup_timeout_ms\": opts.timeoutMs,\n },\n },\n async (span: Span) => {\n try {\n await this._pollUntilWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n onStatus: (update) => this._broadcastReadiness(entry, update),\n signal: sharedController.signal,\n timeoutMs: opts.timeoutMs,\n autoStart: opts.autoStart,\n onWarehouseStartIssued: () => {\n entry.warehouseStartIssued = true;\n },\n },\n span,\n );\n span.setStatus({ code: SpanStatusCode.OK });\n } catch (error) {\n this._throwSanitizedReadinessError(error, warehouseId, span);\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n )\n .finally(() => {\n if (this._readinessInFlight.get(warehouseId) === entry) {\n this._readinessInFlight.delete(warehouseId);\n }\n });\n\n entry.promise.catch(() => {});\n return entry;\n }\n\n private _broadcastReadiness(\n entry: WarehouseReadinessInFlight,\n update: WarehouseStatusUpdate,\n ): void {\n entry.lastUpdate = update;\n for (const subscriber of entry.subscribers) {\n try {\n subscriber(update);\n } catch {\n // One consumer must not block updates to the rest.\n }\n }\n }\n\n /** Ref-counted await; caller abort rejects locally. */\n private _joinWarehouseReadiness(\n entry: WarehouseReadinessInFlight,\n callerSignal?: AbortSignal,\n ): Promise<void> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<void>((resolve, reject) => {\n let done = false;\n const finish = (outcome: \"ok\" | \"abort\" | \"err\", error?: unknown) => {\n if (done) return;\n done = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n if (outcome === \"ok\") resolve();\n else if (outcome === \"abort\") reject(ExecutionError.canceled());\n else reject(error);\n };\n const onAbort = () => {\n this._releaseReadinessWaiter(entry);\n finish(\"abort\");\n };\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n entry.promise.then(\n () => finish(\"ok\"),\n (error) => finish(\"err\", error),\n );\n });\n }\n\n private _releaseReadinessWaiter(entry: WarehouseReadinessInFlight): void {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount > 0 || entry.sharedController.signal.aborted) return;\n\n if (entry.warehouseStartIssued) {\n // Start already issued — finish warming for the process.\n return;\n }\n\n // Defer abort to the next microtask so a synchronous StrictMode\n // remount can rejoin before the shared poll is cancelled.\n queueMicrotask(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\"all warehouse readiness waiters aborted\");\n }\n });\n }\n\n private async _pollUntilWarehouseRunning(\n workspaceClient: WorkspaceClient,\n warehouseId: string,\n opts: EnsureWarehouseRunningOptions,\n span: Span,\n ): Promise<void> {\n const {\n onStatus,\n signal,\n timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n autoStart = true,\n onWarehouseStartIssued,\n } = opts;\n const startTime = Date.now();\n const emitter = new WarehouseStatusEmitter(span, startTime, onStatus);\n let didStart = false;\n const backoff = new WarehousePollBackoff();\n\n while (true) {\n if (signal?.aborted) throw ExecutionError.canceled();\n if (Date.now() - startTime > timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n\n const info = await workspaceClient.warehouses.get(\n { id: warehouseId },\n this._createContext(signal),\n );\n const state = info?.state;\n const summary = info?.health?.summary;\n\n switch (state) {\n case \"RUNNING\":\n emitter.emit(state, summary);\n this._recentlyRunning.set(warehouseId, Date.now());\n span.setAttribute(\"db.warehouse.attempts\", emitter.attempt);\n return;\n case \"DELETED\":\n case \"DELETING\":\n throw ConfigurationError.resourceNotFound(\n \"Warehouse ID\",\n `The configured SQL warehouse is ${state}. Update DATABRICKS_WAREHOUSE_ID to point at an active warehouse.`,\n );\n case \"STOPPED\":\n if (!autoStart) {\n throw new ConfigurationError(\n \"The configured SQL warehouse is STOPPED and analytics auto-start is disabled. Start the warehouse manually or set analytics.autoStartWarehouse=true.\",\n );\n }\n if (!didStart) {\n emitter.emit(\"STARTING\", summary);\n onWarehouseStartIssued?.();\n await workspaceClient.warehouses.start(\n { id: warehouseId },\n this._createContext(signal),\n );\n didStart = true;\n } else {\n emitter.emit(state, summary);\n }\n break;\n case \"STARTING\":\n case \"STOPPING\":\n emitter.emit(state, summary);\n break;\n default:\n throw ExecutionError.unknownState(String(state ?? \"unknown\"));\n }\n\n const sleepMs = backoff.next();\n if (Date.now() + sleepMs - startTime >= timeoutMs) {\n throw ExecutionError.statementFailed(\n `SQL warehouse did not reach RUNNING within ${timeoutMs}ms`,\n );\n }\n await this._sleepRespectingAbort(sleepMs, signal);\n }\n }\n\n /**\n * `true` if this connector observed `warehouseId` in the RUNNING state\n * within the recent-cache TTL.\n */\n private _isRecentlyRunning(warehouseId: string): boolean {\n const observedAt = this._recentlyRunning.get(warehouseId);\n return (\n observedAt !== undefined &&\n Date.now() - observedAt < WAREHOUSE_RUNNING_CACHE_TTL_MS\n );\n }\n\n /**\n * Final landing for any error thrown by the readiness poll loop. Records\n * the original on the span and (at debug level) on the logger, then\n * rethrows either the structured AppKitError unchanged or a curated\n * ExecutionError — never the raw SDK message, which can contain operator\n * internals.\n *\n * The `logger.debug` covers environments where OTel isn't configured\n * (common in dev) so the raw error isn't lost just because no span\n * exporter is hooked up.\n */\n private _throwSanitizedReadinessError(\n error: unknown,\n warehouseId: string,\n span: Span,\n ): never {\n if (error instanceof AppKitError) {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.code });\n throw error;\n }\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n logger.debug(\n \"Warehouse readiness check raw error for %s: %O\",\n warehouseId,\n error,\n );\n const wrapped = ExecutionError.statementFailed(\n \"Warehouse readiness check failed\",\n );\n span.setStatus({ code: SpanStatusCode.ERROR, message: wrapped.code });\n throw wrapped;\n }\n\n /**\n * Sleep for `ms` milliseconds, but resolve early (and reject with\n * `ExecutionError.canceled()`) if `signal` aborts mid-sleep. Used by the\n * warehouse readiness loop so that client disconnects don't keep the loop\n * polling for the full interval.\n */\n private _sleepRespectingAbort(\n ms: number,\n signal?: AbortSignal,\n ): Promise<void> {\n if (signal?.aborted) return Promise.reject(ExecutionError.canceled());\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(ExecutionError.canceled());\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n private async _pollForStatementResult(\n workspaceClient: WorkspaceClient,\n statementId: string,\n timeout = executeStatementDefaults.timeout,\n signal?: AbortSignal,\n ) {\n return this.telemetry.startActiveSpan(\n \"sql.poll\",\n {\n attributes: {\n \"db.statement_id\": statementId,\n \"db.polling.timeout\": timeout,\n },\n },\n async (span: Span) => {\n try {\n const startTime = Date.now();\n let delay = 1000;\n const maxDelayBetweenPolls = 5000; // max 5 seconds between polls\n let pollCount = 0;\n\n while (true) {\n pollCount++;\n span.setAttribute(\"db.polling.current_attempt\", pollCount);\n\n // check if timeout exceeded\n const elapsedTime = Date.now() - startTime;\n if (elapsedTime > timeout) {\n const error = ExecutionError.statementFailed(\n `Polling timeout exceeded after ${timeout}ms (elapsed: ${elapsedTime}ms)`,\n );\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n if (signal?.aborted) {\n const error = ExecutionError.canceled();\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n }\n\n span.addEvent(\"polling.attempt\", {\n \"poll.attempt\": pollCount,\n \"poll.delay_ms\": delay,\n \"poll.elapsed_ms\": elapsedTime,\n });\n\n const response =\n await workspaceClient.statementExecution.getStatement(\n {\n statement_id: statementId,\n },\n this._createContext(signal),\n );\n if (!response) {\n throw ConnectionError.apiFailure(\"SQL Warehouse\");\n }\n\n const status = response.status;\n\n span.addEvent(\"polling.status_check\", {\n \"db.status\": status?.state,\n \"poll.attempt\": pollCount,\n });\n\n switch (status?.state) {\n case \"PENDING\":\n case \"RUNNING\":\n // continue polling\n break;\n case \"SUCCEEDED\":\n span.setAttribute(\"db.polling.attempts\", pollCount);\n span.setAttribute(\"db.polling.total_duration_ms\", elapsedTime);\n span.addEvent(\"polling.completed\", {\n \"poll.attempts\": pollCount,\n \"poll.duration_ms\": elapsedTime,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n return this._transformDataArray(response);\n case \"FAILED\":\n throw ExecutionError.statementFailed(status.error?.message);\n case \"CANCELED\":\n throw ExecutionError.canceled();\n case \"CLOSED\":\n throw ExecutionError.resultsClosed();\n default:\n throw ExecutionError.unknownState(\n String(status?.state ?? \"unknown\"),\n );\n }\n\n // continue polling after delay\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * 2, maxDelayBetweenPolls);\n }\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n\n // error logging is handled by executeStatement's catch block (gated on isAborted)\n if (error instanceof Error && error.name === \"AbortError\") {\n throw error;\n }\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n private _transformDataArray(response: sql.StatementResponse) {\n if (response.manifest?.format === \"ARROW_STREAM\") {\n return this.updateWithArrowStatus(response);\n }\n\n if (!response.result?.data_array || !response.manifest?.schema?.columns) {\n return response;\n }\n\n const columns = response.manifest.schema.columns;\n\n const transformedData = response.result.data_array.map((row) => {\n const obj: Record<string, unknown> = {};\n row.forEach((value, index) => {\n const column = columns[index];\n const columnName = column?.name || `column_${index}`;\n\n // attempt to parse JSON strings for string columns\n if (\n column?.type_name === \"STRING\" &&\n typeof value === \"string\" &&\n value &&\n (value[0] === \"{\" || value[0] === \"[\")\n ) {\n try {\n obj[columnName] = JSON.parse(value);\n } catch {\n // if parsing fails, keep as string\n obj[columnName] = value;\n }\n } else {\n obj[columnName] = value;\n }\n });\n return obj;\n });\n\n // remove data_array\n const { data_array: _data_array, ...restResult } = response.result;\n return {\n ...response,\n result: {\n ...restResult,\n data: transformedData,\n },\n };\n }\n\n private updateWithArrowStatus(response: sql.StatementResponse): {\n result: { statement_id: string; status: sql.StatementStatus };\n } {\n return {\n result: {\n statement_id: response.statement_id as string,\n status: {\n state: response.status?.state,\n error: response.status?.error,\n } as sql.StatementStatus,\n },\n };\n }\n\n async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.arrowProcessor.processChunks>> {\n const startTime = Date.now();\n\n return this.telemetry.startActiveSpan(\n \"arrow.getData\",\n {\n kind: SpanKind.CLIENT,\n attributes: {\n \"db.system\": \"databricks\",\n \"arrow.job_id\": jobId,\n },\n },\n async (span: Span) => {\n try {\n const response =\n await workspaceClient.statementExecution.getStatement(\n { statement_id: jobId },\n this._createContext(signal),\n );\n\n const chunks = response.result?.external_links;\n const schema = response.manifest?.schema;\n\n if (!chunks || !schema) {\n throw ExecutionError.missingData(\"chunks or schema\");\n }\n\n span.setAttribute(\"arrow.chunk_count\", chunks.length);\n\n const result = await this.arrowProcessor.processChunks(\n chunks,\n schema,\n signal,\n );\n\n span.setAttribute(\"arrow.data_size_bytes\", result.data.length);\n span.setStatus({ code: SpanStatusCode.OK });\n\n const duration = Date.now() - startTime;\n this.telemetryMetrics.queryDuration.record(duration, {\n operation: \"arrow.getData\",\n status: \"success\",\n });\n\n logger.event()?.setContext(\"sql-warehouse\", {\n arrow_data_size_bytes: result.data.length,\n arrow_job_id: jobId,\n });\n\n return result;\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : \"Unknown error\",\n });\n span.recordException(error as Error);\n\n const duration = Date.now() - startTime;\n this.telemetryMetrics.queryDuration.record(duration, {\n operation: \"arrow.getData\",\n status: \"error\",\n });\n\n logger.error(\"Failed Arrow job: %s %O\", jobId, error);\n\n if (error instanceof AppKitError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n }\n },\n );\n }\n\n // create context for cancellation token\n private _createContext(signal?: AbortSignal) {\n return new Context({\n cancellationToken: {\n isCancellationRequested: signal?.aborted ?? false,\n onCancellationRequested: (cb: () => void) => {\n signal?.addEventListener(\"abort\", cb, { once: true });\n },\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../src/plugin/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAuNsB,MAAA,iBACJ,gBAAA,GAAmB,gBAAA,aACxB,UAAA;EAAA,UA6BW,MAAA,EAAQ,OAAA;EAAA,UA3BpB,OAAA;EAAA,UACA,KAAA,EAAQ,YAAA;EAAA,UACR,GAAA,EAAK,UAAA;EAAA,UACL,aAAA,EAAe,aAAA;EAAA,UACf,aAAA,EAAe,aAAA;EAAA,UACf,SAAA,EAAY,UAAA;EAAA,UACZ,OAAA,GAAU,aAAA;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../src/plugin/plugin.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAuNsB,MAAA,iBACJ,gBAAA,GAAmB,gBAAA,aACxB,UAAA;EAAA,UA6BW,MAAA,EAAQ,OAAA;EAAA,UA3BpB,OAAA;EAAA,UACA,KAAA,EAAQ,YAAA;EAAA,UACR,GAAA,EAAK,UAAA;EAAA,UACL,aAAA,EAAe,aAAA;EAAA,UACf,aAAA,EAAe,aAAA;EAAA,UACf,SAAA,EAAY,UAAA;EAAA,UACZ,OAAA,GAAU,aAAA;EAwSO;EAAA,QArSnB,mBAAA;EAsSG;EAAA,QAnSH,oBAAA;EAoSN;;;;;;EAAA,OA5RK,KAAA,EAAO,WAAA;EA6W0B;;;EAxWxC,IAAA;cAEsB,MAAA,EAAQ,OAAA;EAAA,QAoBtB,gBAAA;EAqVG;;;;;;;;EAhUX,aAAA,CACE,IAAA;IACE,OAAA;IACA,eAAA,GAAkB,gBAAA;EAAA;EAgBtB,YAAA,CAAa,CAAA,EAAG,OAAA,CAAQ,MAAA;EAIlB,KAAA,CAAA,GAAK,OAAA;EAEX,YAAA,CAAA,GAAgB,iBAAA;EAIhB,uBAAA,CAAA,GAA2B,WAAA;EAI3B,qBAAA,CAAA;EA8ayB;;;;;;;;;;;;;;;;;;;;;;;;EAlZzB,OAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiDA,YAAA,CAAA,GAAgB,MAAA;;;;;;;;;;;YAcN,aAAA,CAAc,GAAA,EAAK,OAAA,CAAQ,OAAA;;;;;;;;;;;EAiBrC,MAAA,CAAO,GAAA,EAAK,OAAA,CAAQ,OAAA;;;;;;;;;;;;;;UAyDZ,kBAAA;EAAA,UAkCQ,aAAA,GAAA,CACd,GAAA,EAAK,YAAA,EACL,EAAA,EAAI,oBAAA,CAAqB,CAAA,GACzB,OAAA,EAAS,uBAAA,EACT,OAAA,YAAgB,OAAA;;;;;;;;;;YAgFF,OAAA,GAAA,CACd,EAAA,GAAK,MAAA,GAAS,WAAA,KAAgB,OAAA,CAAQ,CAAA,GACtC,OAAA,EAAS,uBAAA,EACT,OAAA,YACC,OAAA,CAAQ,eAAA,CAAgB,CAAA;EAAA,UAmDjB,gBAAA,CAAiB,IAAA,UAAc,IAAA;EAAA,UAI/B,KAAA,YAAA,CACR,MAAA,EAAQ,OAAA,CAAQ,MAAA,EAChB,MAAA,EAAQ,WAAA;EAAA,QAeF,qBAAA;EAAA,QAaA,kBAAA;EAAA,QAqCM,wBAAA;EAAA,QAqBN,iBAAA;AAAA"}
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -325,10 +325,10 @@ var Plugin = class {
|
|
|
325
325
|
* @throws AuthenticationError in production when no user header is present.
|
|
326
326
|
*/
|
|
327
327
|
resolveUserId(req) {
|
|
328
|
-
const userId = req.header("x-forwarded-user");
|
|
328
|
+
const userId = req.header("x-forwarded-user")?.trim();
|
|
329
329
|
if (userId) return userId;
|
|
330
330
|
if (process.env.NODE_ENV === "development") return getCurrentUserId();
|
|
331
|
-
throw AuthenticationError.
|
|
331
|
+
throw AuthenticationError.missingUserId();
|
|
332
332
|
}
|
|
333
333
|
/**
|
|
334
334
|
* Execute operations using the user's identity from the request.
|
|
@@ -341,8 +341,8 @@ var Plugin = class {
|
|
|
341
341
|
* In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.
|
|
342
342
|
*/
|
|
343
343
|
asUser(req) {
|
|
344
|
-
const token = req.header("x-forwarded-access-token");
|
|
345
|
-
const userId = req.header("x-forwarded-user");
|
|
344
|
+
const token = req.header("x-forwarded-access-token")?.trim();
|
|
345
|
+
const userId = req.header("x-forwarded-user")?.trim();
|
|
346
346
|
const userEmail = req.header("x-forwarded-email");
|
|
347
347
|
const isDev = process.env.NODE_ENV === "development";
|
|
348
348
|
if (!token && isDev) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["otelContext","context"],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import { createContextKey, context as otelContext } from \"@opentelemetry/api\";\nimport type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport { getCurrentUserId, runInUserContext, ServiceContext } from \"../context\";\nimport type { PluginContext } from \"../core/plugin-context\";\nimport { AppKitError, AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport type { ExecutionResult } from \"./execution-result\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * OTel context key for marking OBO dev mode fallback.\n * Set when asUser() is called in development mode without a user token.\n */\nconst DEV_OBO_FALLBACK_KEY = createContextKey(\"appkit.devOboFallback\");\n\n/**\n * Returns true if `value` is a plain object literal (not an array, Date,\n * class instance, etc.). Used to decide whether to recurse into nested\n * export shapes when wrapping functions.\n *\n * @internal exported so the AppKit core can reuse the same predicate for\n * its `bindExportMethods` walk; not part of the public package surface.\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Returns a deep copy of `exports` where every function has been replaced\n * with `wrap(fn)`, walking into nested plain objects.\n *\n * Used by the asUser proxy to make the user context follow function\n * references that escape the proxy via `exports()`. The original input is\n * not mutated, so plugins that memoize `exports()` are safe — each call\n * through the proxy yields an independent, freshly wrapped view.\n */\nfunction wrapExportFunctions(\n exports: Record<string, unknown>,\n wrap: (fn: (...a: unknown[]) => unknown) => (...a: unknown[]) => unknown,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(exports)) {\n const val = exports[key];\n if (typeof val === \"function\") {\n result[key] = wrap(val as (...a: unknown[]) => unknown);\n } else if (isPlainObject(val)) {\n result[key] = wrapExportFunctions(val, wrap);\n } else {\n result[key] = val;\n }\n }\n return result;\n}\n\n/**\n * Returns true if the current execution is an OBO dev mode fallback\n * (asUser() was called but fell back to service principal due to missing token).\n */\nexport function isDevOboFallback(): boolean {\n return otelContext.active().getValue(DEV_OBO_FALLBACK_KEY) === true;\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n return (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n );\n}\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"attachContext\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n \"clientConfig\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest<\"myPlugin\">;\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache!: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry!: ITelemetry;\n protected context?: PluginContext;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /** Paths that opt out of JSON body parsing (e.g. file upload routes) */\n private skipBodyParsingPaths: Set<string> = new Set();\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name =\n config.name ??\n (this.constructor as { manifest?: { name: string } }).manifest?.name ??\n \"plugin\";\n this.streamManager = new StreamManager();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n this.context = (config as Record<string, unknown>).context as\n | PluginContext\n | undefined;\n\n // Eagerly bind telemetry + cache if the core services have already been\n // initialized (normal createApp path, or tests that mock CacheManager).\n // If they haven't, we leave these undefined and rely on `attachContext`\n // being called later — this lets factories eagerly construct plugin\n // instances at module top-level before `createApp` has run.\n this.tryAttachContext();\n }\n\n private tryAttachContext(): void {\n try {\n this.cache = CacheManager.getInstanceSync();\n } catch {\n return;\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.isReady = true;\n }\n\n /**\n * Binds runtime dependencies (telemetry provider, cache, plugin context) to\n * this plugin. Called by `AppKit._createApp` after construction and before\n * `setup()`. Idempotent: safe to call if the constructor already bound them\n * eagerly. Kept separate so factories can eagerly construct plugin instances\n * without running this before `TelemetryManager.initialize()` /\n * `CacheManager.getInstance()` have run.\n */\n attachContext(\n deps: {\n context?: unknown;\n telemetryConfig?: BasePluginConfig[\"telemetry\"];\n } = {},\n ): void {\n if (!this.cache) {\n this.cache = CacheManager.getInstanceSync();\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n deps.telemetryConfig ?? this.config.telemetry,\n );\n if (deps.context !== undefined) {\n this.context = deps.context as PluginContext;\n }\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n getSkipBodyParsingPaths(): ReadonlySet<string> {\n return this.skipBodyParsingPaths;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Returns startup config to expose to the client.\n * Override this to surface server-side values that are safe to publish to the\n * frontend, such as feature flags, resource IDs, or other app boot settings.\n *\n * This runs once when the server starts, so it should not depend on\n * request-scoped or user-specific state.\n *\n * String values that match non-public environment variables are redacted\n * unless you intentionally expose them via a matching `PUBLIC_APPKIT_` env var.\n *\n * Values must be JSON-serializable plain data (no functions, Dates, classes,\n * Maps, Sets, BigInts, or circular references).\n * By default returns an empty object (plugin contributes nothing to client config).\n *\n * On the client, read the config with the `usePluginClientConfig` hook\n * (React) or the `getPluginClientConfig` function (vanilla JS), both\n * from `@databricks/appkit-ui`.\n *\n * @example\n * ```ts\n * // Server — plugin definition\n * class MyPlugin extends Plugin<MyConfig> {\n * clientConfig() {\n * return {\n * warehouseId: this.config.warehouseId,\n * features: { darkMode: true },\n * };\n * }\n * }\n *\n * // Client — React component\n * import { usePluginClientConfig } from \"@databricks/appkit-ui/react\";\n *\n * interface MyPluginConfig { warehouseId: string; features: { darkMode: boolean } }\n *\n * const config = usePluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * config.warehouseId; // \"abc-123\"\n *\n * // Client — vanilla JS\n * import { getPluginClientConfig } from \"@databricks/appkit-ui/js\";\n *\n * const config = getPluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * ```\n */\n clientConfig(): Record<string, unknown> {\n return {};\n }\n\n /**\n * Resolve the effective user ID from a request.\n *\n * Returns the `x-forwarded-user` header when present. In development mode\n * (`NODE_ENV=development`) falls back to the current context user ID so\n * that callers outside an active `runInUserContext` scope still get a\n * consistent value.\n *\n * @throws AuthenticationError in production when no user header is present.\n */\n protected resolveUserId(req: express.Request): string {\n const userId = req.header(\"x-forwarded-user\");\n if (userId) return userId;\n if (process.env.NODE_ENV === \"development\") return getCurrentUserId();\n throw AuthenticationError.missingToken(\n \"Missing x-forwarded-user header. Cannot resolve user ID.\",\n );\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws AuthenticationError if user token is not available in request headers (production only).\n * In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.\n */\n asUser(req: express.Request): this {\n const token = req.header(\"x-forwarded-access-token\");\n const userId = req.header(\"x-forwarded-user\");\n const userEmail = req.header(\"x-forwarded-email\");\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, skip user impersonation since there's no user\n // token available. Mark execution as OBO dev fallback via OTel context\n // so telemetry can distinguish intended OBO calls from regular SP calls.\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Skipping user impersonation.\",\n );\n\n return this._createAsUserProxy((fn) => (...args) => {\n const ctx = otelContext.active().setValue(DEV_OBO_FALLBACK_KEY, true);\n return otelContext.with(ctx, () => fn(...args));\n });\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n undefined,\n userEmail ?? undefined,\n );\n\n return this._createAsUserProxy(\n (fn) =>\n (...args) =>\n runInUserContext(userContext, () => fn(...args)),\n );\n }\n\n /**\n * Creates a proxy of `this` where every method call — and every function\n * in the result of `exports()` — runs inside `wrapCall`.\n *\n * `wrapCall` decides the per-call scope. Two strategies are used today:\n * - real OBO: fn => (...args) => runInUserContext(userContext, () => fn(...args))\n * - dev fallback: fn => (...args) => otelContext.with(DEV_OBO_FALLBACK_KEY=true, () => fn(...args))\n *\n * `exports` is intercepted because methods captured in the returned\n * exports object never re-enter the proxy's `get` trap. Wrapping them\n * here is the only way to make the user context follow function\n * references back out of the plugin.\n */\n private _createAsUserProxy(\n wrapCall: (\n fn: (...a: unknown[]) => unknown,\n ) => (...a: unknown[]) => unknown,\n ): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") return value;\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop))\n return value;\n\n if (prop === \"exports\") {\n return () => {\n const raw = (value as () => unknown).call(target);\n if (raw == null) return {};\n // Callable exports (e.g. files, jobs) manage per-call asUser\n // themselves; leave them untouched.\n if (typeof raw === \"function\") return raw;\n if (isPlainObject(raw)) {\n return wrapExportFunctions(raw, wrapCall);\n }\n return raw;\n };\n }\n\n const fn = (value as (...a: unknown[]) => unknown).bind(target);\n return wrapCall(fn);\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n // capture the active OTel context (HTTP span) before entering the async generator,\n // where it would otherwise be lost across the async boundary\n const parentOtelContext = otelContext.active();\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors, restoring the parent OTel context\n // so telemetry spans are linked as children of the HTTP request span\n const result = await otelContext.with(parentOtelContext, () =>\n self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n ),\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client. The effective user key is forwarded\n // to the stream manager so that reconnections to existing streamIds are\n // bound to the original creator (prevents cross-user stream takeover via\n // guessed/leaked IDs).\n await this.streamManager.stream(\n res,\n asyncWrapperFn,\n streamConfig,\n effectiveUserKey,\n );\n }\n\n /**\n * Execute a function with the plugin's interceptor chain.\n *\n * Returns an {@link ExecutionResult} discriminated union:\n * - `{ ok: true, data: T }` on success\n * - `{ ok: false, status: number, message: string }` on failure\n *\n * Errors are never thrown — the method is production-safe.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<ExecutionResult<T>> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n const data = await this._executeWithInterceptors(\n fn,\n interceptors,\n context,\n );\n return { ok: true, data };\n } catch (error) {\n logger.error(\"Plugin execution failed\", { error, plugin: this.name });\n\n if (error instanceof AppKitError) {\n return {\n ok: false,\n status: error.statusCode,\n message: error.message,\n };\n }\n\n if (hasHttpStatusCode(error)) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const isClientError = error.statusCode >= 400 && error.statusCode < 500;\n return {\n ok: false,\n status: error.statusCode,\n message: isDev || isClientError ? error.message : \"Server error\",\n };\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n ok: false,\n status: 500,\n message:\n isDev && error instanceof Error ? error.message : \"Server error\",\n };\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n const fullPath = `/api/${this.name}${path}`;\n this.registerEndpoint(name, fullPath);\n\n if (config.skipBodyParsing) {\n this.skipBodyParsingPaths.add(fullPath);\n }\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["otelContext","context"],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import { createContextKey, context as otelContext } from \"@opentelemetry/api\";\nimport type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport { getCurrentUserId, runInUserContext, ServiceContext } from \"../context\";\nimport type { PluginContext } from \"../core/plugin-context\";\nimport { AppKitError, AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport type { ExecutionResult } from \"./execution-result\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * OTel context key for marking OBO dev mode fallback.\n * Set when asUser() is called in development mode without a user token.\n */\nconst DEV_OBO_FALLBACK_KEY = createContextKey(\"appkit.devOboFallback\");\n\n/**\n * Returns true if `value` is a plain object literal (not an array, Date,\n * class instance, etc.). Used to decide whether to recurse into nested\n * export shapes when wrapping functions.\n *\n * @internal exported so the AppKit core can reuse the same predicate for\n * its `bindExportMethods` walk; not part of the public package surface.\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Returns a deep copy of `exports` where every function has been replaced\n * with `wrap(fn)`, walking into nested plain objects.\n *\n * Used by the asUser proxy to make the user context follow function\n * references that escape the proxy via `exports()`. The original input is\n * not mutated, so plugins that memoize `exports()` are safe — each call\n * through the proxy yields an independent, freshly wrapped view.\n */\nfunction wrapExportFunctions(\n exports: Record<string, unknown>,\n wrap: (fn: (...a: unknown[]) => unknown) => (...a: unknown[]) => unknown,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const key of Object.keys(exports)) {\n const val = exports[key];\n if (typeof val === \"function\") {\n result[key] = wrap(val as (...a: unknown[]) => unknown);\n } else if (isPlainObject(val)) {\n result[key] = wrapExportFunctions(val, wrap);\n } else {\n result[key] = val;\n }\n }\n return result;\n}\n\n/**\n * Returns true if the current execution is an OBO dev mode fallback\n * (asUser() was called but fell back to service principal due to missing token).\n */\nexport function isDevOboFallback(): boolean {\n return otelContext.active().getValue(DEV_OBO_FALLBACK_KEY) === true;\n}\n\n/**\n * Narrow an unknown thrown value to an Error that carries a numeric\n * `statusCode` property (e.g. `ApiError` from `@databricks/sdk-experimental`).\n */\nfunction hasHttpStatusCode(\n error: unknown,\n): error is Error & { statusCode: number } {\n return (\n error instanceof Error &&\n \"statusCode\" in error &&\n typeof (error as Record<string, unknown>).statusCode === \"number\"\n );\n}\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"attachContext\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"getSkipBodyParsingPaths\",\n \"abortActiveOperations\",\n \"clientConfig\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\n/**\n * Base abstract class for creating AppKit plugins.\n *\n * All plugins must declare a static `manifest` property with their metadata\n * and resource requirements. The manifest defines:\n * - `required` resources: Always needed for the plugin to function\n * - `optional` resources: May be needed depending on plugin configuration\n *\n * ## Static vs Runtime Resource Requirements\n *\n * The manifest is static and doesn't know the plugin's runtime configuration.\n * For resources that become required based on config options, plugins can\n * implement a static `getResourceRequirements(config)` method.\n *\n * At runtime, this method is called with the actual config to determine\n * which \"optional\" resources should be treated as \"required\".\n *\n * @example Basic plugin with static requirements\n * ```typescript\n * import { Plugin, toPlugin, PluginManifest, ResourceType } from '@databricks/appkit';\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * displayName: 'My Plugin',\n * description: 'Does something awesome',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: []\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest;\n * }\n * ```\n *\n * @example Plugin with config-dependent resources\n * ```typescript\n * interface MyConfig extends BasePluginConfig {\n * enableCaching?: boolean;\n * }\n *\n * const myManifest: PluginManifest = {\n * name: 'myPlugin',\n * resources: {\n * required: [\n * { type: ResourceType.SQL_WAREHOUSE, alias: 'warehouse', ... }\n * ],\n * optional: [\n * // Database is optional in the static manifest\n * { type: ResourceType.DATABASE, alias: 'cache', description: 'Required if caching enabled', ... }\n * ]\n * }\n * };\n *\n * class MyPlugin extends Plugin<MyConfig> {\n * static manifest = myManifest<\"myPlugin\">;\n *\n * // Runtime method: converts optional resources to required based on config\n * static getResourceRequirements(config: MyConfig) {\n * const resources = [];\n * if (config.enableCaching) {\n * // When caching is enabled, Database becomes required\n * resources.push({\n * type: ResourceType.DATABASE,\n * alias: 'cache',\n * resourceKey: 'database',\n * description: 'Cache storage for query results',\n * permission: 'CAN_CONNECT_AND_CREATE',\n * fields: {\n * instance_name: { env: 'DATABRICKS_CACHE_INSTANCE' },\n * database_name: { env: 'DATABRICKS_CACHE_DB' },\n * },\n * required: true // Mark as required at runtime\n * });\n * }\n * return resources;\n * }\n * }\n * ```\n */\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache!: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry!: ITelemetry;\n protected context?: PluginContext;\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n /** Paths that opt out of JSON body parsing (e.g. file upload routes) */\n private skipBodyParsingPaths: Set<string> = new Set();\n\n /**\n * Plugin initialization phase.\n * - 'core': Initialized first (e.g., config plugins)\n * - 'normal': Initialized second (most plugins)\n * - 'deferred': Initialized last (e.g., server plugin)\n */\n static phase: PluginPhase = \"normal\";\n\n /**\n * Plugin name identifier.\n */\n name: string;\n\n constructor(protected config: TConfig) {\n this.name =\n config.name ??\n (this.constructor as { manifest?: { name: string } }).manifest?.name ??\n \"plugin\";\n this.streamManager = new StreamManager();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n this.context = (config as Record<string, unknown>).context as\n | PluginContext\n | undefined;\n\n // Eagerly bind telemetry + cache if the core services have already been\n // initialized (normal createApp path, or tests that mock CacheManager).\n // If they haven't, we leave these undefined and rely on `attachContext`\n // being called later — this lets factories eagerly construct plugin\n // instances at module top-level before `createApp` has run.\n this.tryAttachContext();\n }\n\n private tryAttachContext(): void {\n try {\n this.cache = CacheManager.getInstanceSync();\n } catch {\n return;\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.isReady = true;\n }\n\n /**\n * Binds runtime dependencies (telemetry provider, cache, plugin context) to\n * this plugin. Called by `AppKit._createApp` after construction and before\n * `setup()`. Idempotent: safe to call if the constructor already bound them\n * eagerly. Kept separate so factories can eagerly construct plugin instances\n * without running this before `TelemetryManager.initialize()` /\n * `CacheManager.getInstance()` have run.\n */\n attachContext(\n deps: {\n context?: unknown;\n telemetryConfig?: BasePluginConfig[\"telemetry\"];\n } = {},\n ): void {\n if (!this.cache) {\n this.cache = CacheManager.getInstanceSync();\n }\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n deps.telemetryConfig ?? this.config.telemetry,\n );\n if (deps.context !== undefined) {\n this.context = deps.context as PluginContext;\n }\n this.isReady = true;\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n getSkipBodyParsingPaths(): ReadonlySet<string> {\n return this.skipBodyParsingPaths;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Returns the public exports for this plugin.\n * Override this to define a custom public API.\n * By default, returns an empty object.\n *\n * The returned object becomes the plugin's public API on the AppKit instance\n * (e.g. `appkit.myPlugin.method()`). AppKit automatically binds method context\n * and adds `asUser(req)` for user-scoped execution.\n *\n * @example\n * ```ts\n * class MyPlugin extends Plugin {\n * private getData() { return []; }\n *\n * exports() {\n * return { getData: this.getData };\n * }\n * }\n *\n * // After registration:\n * const appkit = await createApp({ plugins: [myPlugin()] });\n * appkit.myPlugin.getData();\n * ```\n */\n exports(): unknown {\n return {};\n }\n\n /**\n * Returns startup config to expose to the client.\n * Override this to surface server-side values that are safe to publish to the\n * frontend, such as feature flags, resource IDs, or other app boot settings.\n *\n * This runs once when the server starts, so it should not depend on\n * request-scoped or user-specific state.\n *\n * String values that match non-public environment variables are redacted\n * unless you intentionally expose them via a matching `PUBLIC_APPKIT_` env var.\n *\n * Values must be JSON-serializable plain data (no functions, Dates, classes,\n * Maps, Sets, BigInts, or circular references).\n * By default returns an empty object (plugin contributes nothing to client config).\n *\n * On the client, read the config with the `usePluginClientConfig` hook\n * (React) or the `getPluginClientConfig` function (vanilla JS), both\n * from `@databricks/appkit-ui`.\n *\n * @example\n * ```ts\n * // Server — plugin definition\n * class MyPlugin extends Plugin<MyConfig> {\n * clientConfig() {\n * return {\n * warehouseId: this.config.warehouseId,\n * features: { darkMode: true },\n * };\n * }\n * }\n *\n * // Client — React component\n * import { usePluginClientConfig } from \"@databricks/appkit-ui/react\";\n *\n * interface MyPluginConfig { warehouseId: string; features: { darkMode: boolean } }\n *\n * const config = usePluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * config.warehouseId; // \"abc-123\"\n *\n * // Client — vanilla JS\n * import { getPluginClientConfig } from \"@databricks/appkit-ui/js\";\n *\n * const config = getPluginClientConfig<MyPluginConfig>(\"myPlugin\");\n * ```\n */\n clientConfig(): Record<string, unknown> {\n return {};\n }\n\n /**\n * Resolve the effective user ID from a request.\n *\n * Returns the `x-forwarded-user` header when present. In development mode\n * (`NODE_ENV=development`) falls back to the current context user ID so\n * that callers outside an active `runInUserContext` scope still get a\n * consistent value.\n *\n * @throws AuthenticationError in production when no user header is present.\n */\n protected resolveUserId(req: express.Request): string {\n const userId = req.header(\"x-forwarded-user\")?.trim();\n if (userId) return userId;\n if (process.env.NODE_ENV === \"development\") return getCurrentUserId();\n throw AuthenticationError.missingUserId();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n * Returns a proxy of this plugin where all method calls execute\n * with the user's Databricks credentials instead of the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A proxied plugin instance that executes as the user\n * @throws AuthenticationError if user token is not available in request headers (production only).\n * In development mode (`NODE_ENV=development`), skips user impersonation instead of throwing.\n */\n asUser(req: express.Request): this {\n const token = req.header(\"x-forwarded-access-token\")?.trim();\n const userId = req.header(\"x-forwarded-user\")?.trim();\n const userEmail = req.header(\"x-forwarded-email\");\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, skip user impersonation since there's no user\n // token available. Mark execution as OBO dev fallback via OTel context\n // so telemetry can distinguish intended OBO calls from regular SP calls.\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Skipping user impersonation.\",\n );\n\n return this._createAsUserProxy((fn) => (...args) => {\n const ctx = otelContext.active().setValue(DEV_OBO_FALLBACK_KEY, true);\n return otelContext.with(ctx, () => fn(...args));\n });\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n undefined,\n userEmail ?? undefined,\n );\n\n return this._createAsUserProxy(\n (fn) =>\n (...args) =>\n runInUserContext(userContext, () => fn(...args)),\n );\n }\n\n /**\n * Creates a proxy of `this` where every method call — and every function\n * in the result of `exports()` — runs inside `wrapCall`.\n *\n * `wrapCall` decides the per-call scope. Two strategies are used today:\n * - real OBO: fn => (...args) => runInUserContext(userContext, () => fn(...args))\n * - dev fallback: fn => (...args) => otelContext.with(DEV_OBO_FALLBACK_KEY=true, () => fn(...args))\n *\n * `exports` is intercepted because methods captured in the returned\n * exports object never re-enter the proxy's `get` trap. Wrapping them\n * here is the only way to make the user context follow function\n * references back out of the plugin.\n */\n private _createAsUserProxy(\n wrapCall: (\n fn: (...a: unknown[]) => unknown,\n ) => (...a: unknown[]) => unknown,\n ): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") return value;\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop))\n return value;\n\n if (prop === \"exports\") {\n return () => {\n const raw = (value as () => unknown).call(target);\n if (raw == null) return {};\n // Callable exports (e.g. files, jobs) manage per-call asUser\n // themselves; leave them untouched.\n if (typeof raw === \"function\") return raw;\n if (isPlainObject(raw)) {\n return wrapExportFunctions(raw, wrapCall);\n }\n return raw;\n };\n }\n\n const fn = (value as (...a: unknown[]) => unknown).bind(target);\n return wrapCall(fn);\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n // capture the active OTel context (HTTP span) before entering the async generator,\n // where it would otherwise be lost across the async boundary\n const parentOtelContext = otelContext.active();\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors, restoring the parent OTel context\n // so telemetry spans are linked as children of the HTTP request span\n const result = await otelContext.with(parentOtelContext, () =>\n self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n ),\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client. The effective user key is forwarded\n // to the stream manager so that reconnections to existing streamIds are\n // bound to the original creator (prevents cross-user stream takeover via\n // guessed/leaked IDs).\n await this.streamManager.stream(\n res,\n asyncWrapperFn,\n streamConfig,\n effectiveUserKey,\n );\n }\n\n /**\n * Execute a function with the plugin's interceptor chain.\n *\n * Returns an {@link ExecutionResult} discriminated union:\n * - `{ ok: true, data: T }` on success\n * - `{ ok: false, status: number, message: string }` on failure\n *\n * Errors are never thrown — the method is production-safe.\n */\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<ExecutionResult<T>> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n const data = await this._executeWithInterceptors(\n fn,\n interceptors,\n context,\n );\n return { ok: true, data };\n } catch (error) {\n logger.error(\"Plugin execution failed\", { error, plugin: this.name });\n\n if (error instanceof AppKitError) {\n return {\n ok: false,\n status: error.statusCode,\n message: error.message,\n };\n }\n\n if (hasHttpStatusCode(error)) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const isClientError = error.statusCode >= 400 && error.statusCode < 500;\n return {\n ok: false,\n status: error.statusCode,\n message: isDev || isClientError ? error.message : \"Server error\",\n };\n }\n\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n ok: false,\n status: 500,\n message:\n isDev && error instanceof Error ? error.message : \"Server error\",\n };\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n const fullPath = `/api/${this.name}${path}`;\n this.registerEndpoint(name, fullPath);\n\n if (config.skipBodyParsing) {\n this.skipBodyParsingPaths.add(fullPath);\n }\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|