@databricks/appkit 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +1 -2
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts +27 -5
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +87 -14
- package/dist/cache/index.js.map +1 -1
- package/dist/connectors/index.js +1 -1
- package/dist/connectors/sql-warehouse/client.js +168 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/index.js +1 -1
- package/dist/connectors/sql-warehouse/warehouse-poll-backoff.js +24 -0
- package/dist/connectors/sql-warehouse/warehouse-poll-backoff.js.map +1 -0
- package/dist/connectors/sql-warehouse/warehouse-status-emitter.js +40 -0
- package/dist/connectors/sql-warehouse/warehouse-status-emitter.js.map +1 -0
- package/dist/plugin/interceptors/cache.js +13 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +73 -8
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/manifest.js +17 -5
- package/dist/plugins/analytics/types.d.ts +13 -0
- package/dist/plugins/analytics/types.d.ts.map +1 -1
- package/dist/plugins/analytics/types.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 +3 -3
- package/dist/stream/stream-manager.js.map +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/NOTICE.md
CHANGED
|
@@ -49,10 +49,9 @@ This Software contains code from the following open source projects:
|
|
|
49
49
|
| [@radix-ui/react-toggle](https://www.npmjs.com/package/@radix-ui/react-toggle) | 1.1.10 | MIT | https://radix-ui.com/primitives |
|
|
50
50
|
| [@radix-ui/react-toggle-group](https://www.npmjs.com/package/@radix-ui/react-toggle-group) | 1.1.11 | MIT | https://radix-ui.com/primitives |
|
|
51
51
|
| [@radix-ui/react-tooltip](https://www.npmjs.com/package/@radix-ui/react-tooltip) | 1.2.8 | MIT | https://radix-ui.com/primitives |
|
|
52
|
+
| [@standard-schema/spec](https://www.npmjs.com/package/@standard-schema/spec) | 1.1.0 | MIT | https://standardschema.dev |
|
|
52
53
|
| [@tanstack/react-table](https://www.npmjs.com/package/@tanstack/react-table) | 8.21.3 | MIT | https://tanstack.com/table |
|
|
53
54
|
| [@types/semver](https://www.npmjs.com/package/@types/semver) | 7.7.1 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/semver |
|
|
54
|
-
| [ajv](https://www.npmjs.com/package/ajv) | 6.12.6, 8.17.1 | MIT | https://ajv.js.org |
|
|
55
|
-
| [ajv-formats](https://www.npmjs.com/package/ajv-formats) | 2.1.1, 3.0.1 | MIT | https://github.com/ajv-validator/ajv-formats#readme |
|
|
56
55
|
| [apache-arrow](https://www.npmjs.com/package/apache-arrow) | 21.1.0 | Apache-2.0 | https://arrow.apache.org/js/ |
|
|
57
56
|
| [class-variance-authority](https://www.npmjs.com/package/class-variance-authority) | 0.7.1 | Apache-2.0 | https://github.com/joe-bell/cva#readme |
|
|
58
57
|
| [clsx](https://www.npmjs.com/package/clsx) | 2.1.1 | MIT | https://github.com/lukeed/clsx#readme |
|
package/dist/appkit/package.js
CHANGED
package/dist/cache/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import "../shared/src/index.js";
|
|
|
18
18
|
*/
|
|
19
19
|
declare class CacheManager {
|
|
20
20
|
private static readonly MIN_CLEANUP_INTERVAL_MS;
|
|
21
|
+
private static readonly ABORT_GRACE_PERIOD_MS;
|
|
21
22
|
private readonly name;
|
|
22
23
|
private static instance;
|
|
23
24
|
private static initPromise;
|
|
@@ -58,16 +59,37 @@ declare class CacheManager {
|
|
|
58
59
|
*/
|
|
59
60
|
private static create;
|
|
60
61
|
/**
|
|
61
|
-
* Get or execute a function and cache the result
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* Get or execute a function and cache the result.
|
|
63
|
+
*
|
|
64
|
+
* Multiple concurrent callers with the same `cacheKey` are deduplicated
|
|
65
|
+
* onto a single in-flight execution. Each caller may pass its own
|
|
66
|
+
* `callerSignal`; the underlying `fn()` is run with a shared, internally
|
|
67
|
+
* managed `AbortSignal` that aborts only when *all* callers have
|
|
68
|
+
* abandoned the request (reference counted). This decouples a single
|
|
69
|
+
* caller's cancellation (e.g. React StrictMode unmount) from the shared
|
|
70
|
+
* result, so other still-connected callers receive the cached value
|
|
71
|
+
* normally.
|
|
72
|
+
*
|
|
73
|
+
* @param key - Cache key parts
|
|
74
|
+
* @param fn - Function to execute. Receives the cache-owned shared signal;
|
|
75
|
+
* pass it through to the underlying I/O so the work is cancelled when
|
|
76
|
+
* no caller is left waiting.
|
|
77
|
+
* @param userKey - User key for cache namespacing
|
|
65
78
|
* @param options - Options for the cache
|
|
66
79
|
* @returns Promise of the result
|
|
67
80
|
*/
|
|
68
|
-
getOrExecute<T>(key: (string | number | object)[], fn: () => Promise<T>, userKey: string, options?: {
|
|
81
|
+
getOrExecute<T>(key: (string | number | object)[], fn: (sharedSignal?: AbortSignal) => Promise<T>, userKey: string, options?: {
|
|
69
82
|
ttl?: number;
|
|
83
|
+
callerSignal?: AbortSignal;
|
|
70
84
|
}): Promise<T>;
|
|
85
|
+
/**
|
|
86
|
+
* Wait on an in-flight entry, racing the underlying promise against the
|
|
87
|
+
* caller's abort signal. When the caller aborts, the entry's refCount is
|
|
88
|
+
* decremented; if it hits zero the shared controller is aborted so the
|
|
89
|
+
* underlying `fn()` can stop. Other callers continue to await the same
|
|
90
|
+
* entry and receive the result when it arrives.
|
|
91
|
+
*/
|
|
92
|
+
private _waitWithRefCount;
|
|
71
93
|
/**
|
|
72
94
|
* Get a cached value
|
|
73
95
|
* @param key - Cache key
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"mappings":";;;;;;;AAmDA;;;;;;;;;;;cAAa,YAAA;EAAA,wBACa,uBAAA;EAAA,wBACA,qBAAA;EAAA,iBACP,IAAA;EAAA,eACF,QAAA;EAAA,eACA,WAAA;EAAA,QAEP,OAAA;EAAA,QACA,MAAA;EAAA,QACA,gBAAA;EAAA,QACA,iBAAA;EAAA,QACA,kBAAA;EAAA,QAEA,SAAA;EAAA,QACA,gBAAA;EAAA,QAKD,WAAA,CAAA;EAlBiB;;;;;;EAAA,OA+CjB,eAAA,CAAA,GAAmB,YAAA;EAvClB;;;;;;;EAAA,OAyDK,WAAA,CACX,UAAA,GAAa,OAAA,CAAQ,WAAA,IACpB,OAAA,CAAQ,YAAA;EAFE;;;;;;;;;;;;EAAA,eA+BQ,MAAA;EAyEyB;;;;;;;;;;;;;;;;;;;;EAFxC,YAAA,GAAA,CACJ,GAAA,gCACA,EAAA,GAAK,YAAA,GAAe,WAAA,KAAgB,OAAA,CAAQ,CAAA,GAC5C,OAAA,UACA,OAAA;IAAY,GAAA;IAAc,YAAA,GAAe,WAAA;EAAA,IACxC,OAAA,CAAQ,CAAA;EAkRR;;;;;;;EAAA,QA7HK,iBAAA;EA6JgB;;;;;EAhGlB,GAAA,GAAA,CAAO,GAAA,WAAc,OAAA,CAAQ,CAAA;EA4H7B;;;;;;;EAAA,QA3GQ,QAAA;;UAYN,YAAA;;;;;;;;EA+BF,GAAA,GAAA,CACJ,GAAA,UACA,KAAA,EAAO,CAAA,EACP,OAAA;IAAY,GAAA;EAAA,IACX,OAAA;;;;;;EAaG,MAAA,CAAO,GAAA,WAAc,OAAA;;EAMrB,KAAA,CAAA,GAAS,OAAA;;;;;;EAaT,GAAA,CAAI,GAAA,WAAc,OAAA;;;;;;;EAaxB,WAAA,CAAY,KAAA,gCAAqC,OAAA;;EAO3C,KAAA,CAAA,GAAS,OAAA;;;;;EAQT,gBAAA,CAAA,GAAoB,OAAA;AAAA"}
|
package/dist/cache/index.js
CHANGED
|
@@ -16,6 +16,10 @@ import { ApiError, WorkspaceClient } from "@databricks/sdk-experimental";
|
|
|
16
16
|
|
|
17
17
|
//#region src/cache/index.ts
|
|
18
18
|
const logger = createLogger("cache");
|
|
19
|
+
function createAbortError(signal) {
|
|
20
|
+
if (signal.reason !== void 0) return signal.reason;
|
|
21
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
22
|
+
}
|
|
19
23
|
/**
|
|
20
24
|
* Cache manager class to handle cache operations.
|
|
21
25
|
* Can be used with in-memory storage or persistent storage (Lakebase).
|
|
@@ -32,6 +36,7 @@ const logger = createLogger("cache");
|
|
|
32
36
|
*/
|
|
33
37
|
var CacheManager = class CacheManager {
|
|
34
38
|
static MIN_CLEANUP_INTERVAL_MS = 6e4;
|
|
39
|
+
static ABORT_GRACE_PERIOD_MS = 100;
|
|
35
40
|
name = "cache-manager";
|
|
36
41
|
static instance = null;
|
|
37
42
|
static initPromise = null;
|
|
@@ -129,15 +134,29 @@ var CacheManager = class CacheManager {
|
|
|
129
134
|
return new CacheManager(new InMemoryStorage(config), config);
|
|
130
135
|
}
|
|
131
136
|
/**
|
|
132
|
-
* Get or execute a function and cache the result
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
137
|
+
* Get or execute a function and cache the result.
|
|
138
|
+
*
|
|
139
|
+
* Multiple concurrent callers with the same `cacheKey` are deduplicated
|
|
140
|
+
* onto a single in-flight execution. Each caller may pass its own
|
|
141
|
+
* `callerSignal`; the underlying `fn()` is run with a shared, internally
|
|
142
|
+
* managed `AbortSignal` that aborts only when *all* callers have
|
|
143
|
+
* abandoned the request (reference counted). This decouples a single
|
|
144
|
+
* caller's cancellation (e.g. React StrictMode unmount) from the shared
|
|
145
|
+
* result, so other still-connected callers receive the cached value
|
|
146
|
+
* normally.
|
|
147
|
+
*
|
|
148
|
+
* @param key - Cache key parts
|
|
149
|
+
* @param fn - Function to execute. Receives the cache-owned shared signal;
|
|
150
|
+
* pass it through to the underlying I/O so the work is cancelled when
|
|
151
|
+
* no caller is left waiting.
|
|
152
|
+
* @param userKey - User key for cache namespacing
|
|
136
153
|
* @param options - Options for the cache
|
|
137
154
|
* @returns Promise of the result
|
|
138
155
|
*/
|
|
139
156
|
async getOrExecute(key, fn, userKey, options) {
|
|
140
|
-
if (!this.config.enabled) return fn();
|
|
157
|
+
if (!this.config.enabled) return fn(options?.callerSignal);
|
|
158
|
+
const callerSignal = options?.callerSignal;
|
|
159
|
+
if (callerSignal?.aborted) throw createAbortError(callerSignal);
|
|
141
160
|
const cacheKey = this.generateKey(key, userKey);
|
|
142
161
|
return this.telemetry.startActiveSpan("cache.getOrExecute", { attributes: {
|
|
143
162
|
"cache.key": cacheKey,
|
|
@@ -156,12 +175,16 @@ var CacheManager = class CacheManager {
|
|
|
156
175
|
});
|
|
157
176
|
return cached.value;
|
|
158
177
|
}
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
178
|
+
const existing = this.inFlightRequests.get(cacheKey);
|
|
179
|
+
if (existing && !existing.sharedController.signal.aborted) {
|
|
180
|
+
existing.refCount++;
|
|
181
|
+
if (existing.abortTimer) {
|
|
182
|
+
clearTimeout(existing.abortTimer);
|
|
183
|
+
existing.abortTimer = void 0;
|
|
184
|
+
}
|
|
161
185
|
span.setAttribute("cache.hit", true);
|
|
162
186
|
span.setAttribute("cache.deduplication", true);
|
|
163
187
|
span.addEvent("cache.deduplication_used", { "cache.key": cacheKey });
|
|
164
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
165
188
|
this.telemetryMetrics.cacheHitCount.add(1, {
|
|
166
189
|
"cache.key": cacheKey,
|
|
167
190
|
"cache.deduplication": "true"
|
|
@@ -171,8 +194,7 @@ var CacheManager = class CacheManager {
|
|
|
171
194
|
cache_key: cacheKey,
|
|
172
195
|
cache_deduplication: true
|
|
173
196
|
});
|
|
174
|
-
|
|
175
|
-
return inFlight;
|
|
197
|
+
return await this._waitWithRefCount(existing, callerSignal);
|
|
176
198
|
}
|
|
177
199
|
span.setAttribute("cache.hit", false);
|
|
178
200
|
span.addEvent("cache.miss", { "cache.key": cacheKey });
|
|
@@ -181,7 +203,13 @@ var CacheManager = class CacheManager {
|
|
|
181
203
|
cache_hit: false,
|
|
182
204
|
cache_key: cacheKey
|
|
183
205
|
});
|
|
184
|
-
const
|
|
206
|
+
const sharedController = new AbortController();
|
|
207
|
+
const entry = {
|
|
208
|
+
promise: void 0,
|
|
209
|
+
refCount: 1,
|
|
210
|
+
sharedController
|
|
211
|
+
};
|
|
212
|
+
entry.promise = fn(sharedController.signal).then(async (result) => {
|
|
185
213
|
await this.set(cacheKey, result, options);
|
|
186
214
|
span.addEvent("cache.value_stored", {
|
|
187
215
|
"cache.key": cacheKey,
|
|
@@ -191,13 +219,15 @@ var CacheManager = class CacheManager {
|
|
|
191
219
|
}).catch((error) => {
|
|
192
220
|
span.recordException(error);
|
|
193
221
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
222
|
+
if (sharedController.signal.aborted) throw error;
|
|
194
223
|
if (error instanceof AppKitError || error instanceof ApiError) throw error;
|
|
195
224
|
throw ExecutionError.statementFailed(error instanceof Error ? error.message : String(error));
|
|
196
225
|
}).finally(() => {
|
|
197
|
-
this.inFlightRequests.delete(cacheKey);
|
|
226
|
+
if (this.inFlightRequests.get(cacheKey) === entry) this.inFlightRequests.delete(cacheKey);
|
|
198
227
|
});
|
|
199
|
-
|
|
200
|
-
|
|
228
|
+
entry.promise.catch(() => {});
|
|
229
|
+
this.inFlightRequests.set(cacheKey, entry);
|
|
230
|
+
const result = await this._waitWithRefCount(entry, callerSignal);
|
|
201
231
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
202
232
|
return result;
|
|
203
233
|
} catch (error) {
|
|
@@ -213,6 +243,48 @@ var CacheManager = class CacheManager {
|
|
|
213
243
|
});
|
|
214
244
|
}
|
|
215
245
|
/**
|
|
246
|
+
* Wait on an in-flight entry, racing the underlying promise against the
|
|
247
|
+
* caller's abort signal. When the caller aborts, the entry's refCount is
|
|
248
|
+
* decremented; if it hits zero the shared controller is aborted so the
|
|
249
|
+
* underlying `fn()` can stop. Other callers continue to await the same
|
|
250
|
+
* entry and receive the result when it arrives.
|
|
251
|
+
*/
|
|
252
|
+
_waitWithRefCount(entry, callerSignal) {
|
|
253
|
+
if (!callerSignal) return entry.promise;
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
let settled = false;
|
|
256
|
+
const release = () => {
|
|
257
|
+
if (entry.refCount > 0) entry.refCount--;
|
|
258
|
+
if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) entry.abortTimer = setTimeout(() => {
|
|
259
|
+
if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) entry.sharedController.abort(callerSignal.reason ?? "all cache callers aborted");
|
|
260
|
+
}, CacheManager.ABORT_GRACE_PERIOD_MS);
|
|
261
|
+
};
|
|
262
|
+
const onAbort = () => {
|
|
263
|
+
if (settled) return;
|
|
264
|
+
settled = true;
|
|
265
|
+
callerSignal.removeEventListener("abort", onAbort);
|
|
266
|
+
release();
|
|
267
|
+
reject(createAbortError(callerSignal));
|
|
268
|
+
};
|
|
269
|
+
if (callerSignal.aborted) {
|
|
270
|
+
onAbort();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
274
|
+
entry.promise.then((value) => {
|
|
275
|
+
if (settled) return;
|
|
276
|
+
settled = true;
|
|
277
|
+
callerSignal.removeEventListener("abort", onAbort);
|
|
278
|
+
resolve(value);
|
|
279
|
+
}, (error) => {
|
|
280
|
+
if (settled) return;
|
|
281
|
+
settled = true;
|
|
282
|
+
callerSignal.removeEventListener("abort", onAbort);
|
|
283
|
+
reject(error);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
216
288
|
* Get a cached value
|
|
217
289
|
* @param key - Cache key
|
|
218
290
|
* @returns Promise of the value or null if not found or expired
|
|
@@ -282,6 +354,7 @@ var CacheManager = class CacheManager {
|
|
|
282
354
|
/** Clear the cache */
|
|
283
355
|
async clear() {
|
|
284
356
|
await this.storage.clear();
|
|
357
|
+
for (const entry of this.inFlightRequests.values()) if (entry.abortTimer) clearTimeout(entry.abortTimer);
|
|
285
358
|
this.inFlightRequests.clear();
|
|
286
359
|
}
|
|
287
360
|
/**
|
package/dist/cache/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { ApiError, WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, Promise<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({});\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result\n * @param key - Cache key\n * @param fn - Function to execute\n * @param userKey - User key\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: () => Promise<T>,\n userKey: string,\n options?: { ttl?: number },\n ): Promise<T> {\n if (!this.config.enabled) return fn();\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n const cached = await this.getValid<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value;\n }\n\n // check if the value is being processed by another request\n const inFlight = this.inFlightRequests.get(cacheKey);\n if (inFlight) {\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n span.end();\n return inFlight as Promise<T>;\n }\n\n // cache miss - execute function\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const promise = fn()\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // Preserve AppKit errors and Databricks API errors (with status codes)\n // so route handlers can map them to proper HTTP responses.\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n this.inFlightRequests.delete(cacheKey);\n });\n\n this.inFlightRequests.set(cacheKey, promise);\n\n const result = await promise;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.getValid(key);\n return entry !== null;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { ApiError, WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Reference-counted in-flight cache execution entry.\n *\n * `sharedController` decouples the cached `fn()` from any single caller's\n * abort signal. Callers join an in-flight entry by incrementing `refCount`;\n * when a caller aborts, refCount is decremented. The shared controller is\n * aborted only when refCount drops to 0 — i.e. all callers have abandoned\n * the request. This prevents one caller's cancellation (e.g. React\n * StrictMode unmount) from poisoning the in-flight result for other still-\n * connected awaiters.\n */\ninterface InFlightEntry<T> {\n promise: Promise<T>;\n refCount: number;\n sharedController: AbortController;\n abortTimer?: ReturnType<typeof setTimeout>;\n}\n\nfunction createAbortError(signal: AbortSignal): unknown {\n if (signal.reason !== undefined) return signal.reason;\n return new DOMException(\"The operation was aborted.\", \"AbortError\");\n}\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private static readonly ABORT_GRACE_PERIOD_MS = 100;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, InFlightEntry<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({});\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result.\n *\n * Multiple concurrent callers with the same `cacheKey` are deduplicated\n * onto a single in-flight execution. Each caller may pass its own\n * `callerSignal`; the underlying `fn()` is run with a shared, internally\n * managed `AbortSignal` that aborts only when *all* callers have\n * abandoned the request (reference counted). This decouples a single\n * caller's cancellation (e.g. React StrictMode unmount) from the shared\n * result, so other still-connected callers receive the cached value\n * normally.\n *\n * @param key - Cache key parts\n * @param fn - Function to execute. Receives the cache-owned shared signal;\n * pass it through to the underlying I/O so the work is cancelled when\n * no caller is left waiting.\n * @param userKey - User key for cache namespacing\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: (sharedSignal?: AbortSignal) => Promise<T>,\n userKey: string,\n options?: { ttl?: number; callerSignal?: AbortSignal },\n ): Promise<T> {\n if (!this.config.enabled) return fn(options?.callerSignal);\n\n const callerSignal = options?.callerSignal;\n if (callerSignal?.aborted) {\n throw createAbortError(callerSignal);\n }\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n const cached = await this.getValid<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value;\n }\n\n // check if the value is being processed by another request — join\n // the existing in-flight entry under reference counting so this\n // caller's abort doesn't poison the shared result.\n const existing = this.inFlightRequests.get(cacheKey) as\n | InFlightEntry<T>\n | undefined;\n if (existing && !existing.sharedController.signal.aborted) {\n existing.refCount++;\n // Cancel any pending abort timer — a new caller has joined\n if (existing.abortTimer) {\n clearTimeout(existing.abortTimer);\n existing.abortTimer = undefined;\n }\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n return await this._waitWithRefCount(existing, callerSignal);\n }\n\n // cache miss - execute function under a shared abort controller\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const sharedController = new AbortController();\n const entry: InFlightEntry<T> = {\n promise: undefined as unknown as Promise<T>,\n refCount: 1,\n sharedController,\n };\n\n entry.promise = fn(sharedController.signal)\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // If the shared controller aborted, all callers have already\n // abandoned the request (or are about to via their own signals)\n // — propagate the original error without wrapping. No live\n // awaiter will observe this rejection.\n if (sharedController.signal.aborted) {\n throw error;\n }\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n if (this.inFlightRequests.get(cacheKey) === entry) {\n this.inFlightRequests.delete(cacheKey);\n }\n });\n\n // Suppress unhandled rejection warnings when every caller bailed\n // before fn() resolved (their own promises rejected via\n // waitWithRefCount; the underlying entry.promise has no awaiter).\n entry.promise.catch(() => {});\n\n this.inFlightRequests.set(cacheKey, entry as InFlightEntry<unknown>);\n\n const result = await this._waitWithRefCount(entry, callerSignal);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Wait on an in-flight entry, racing the underlying promise against the\n * caller's abort signal. When the caller aborts, the entry's refCount is\n * decremented; if it hits zero the shared controller is aborted so the\n * underlying `fn()` can stop. Other callers continue to await the same\n * entry and receive the result when it arrives.\n */\n private _waitWithRefCount<T>(\n entry: InFlightEntry<T>,\n callerSignal?: AbortSignal,\n ): Promise<T> {\n if (!callerSignal) return entry.promise;\n\n return new Promise<T>((resolve, reject) => {\n let settled = false;\n\n const release = () => {\n if (entry.refCount > 0) entry.refCount--;\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n // Grace period: delay abort so a StrictMode remount can join\n // the in-flight entry before the shared execution is cancelled.\n entry.abortTimer = setTimeout(() => {\n if (entry.refCount <= 0 && !entry.sharedController.signal.aborted) {\n entry.sharedController.abort(\n callerSignal.reason ?? \"all cache callers aborted\",\n );\n }\n }, CacheManager.ABORT_GRACE_PERIOD_MS);\n }\n };\n\n const onAbort = () => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n release();\n reject(createAbortError(callerSignal));\n };\n\n if (callerSignal.aborted) {\n onAbort();\n return;\n }\n\n callerSignal.addEventListener(\"abort\", onAbort, { once: true });\n\n entry.promise.then(\n (value) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n resolve(value);\n },\n (error) => {\n if (settled) return;\n settled = true;\n callerSignal.removeEventListener(\"abort\", onAbort);\n reject(error);\n },\n );\n });\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n for (const entry of this.inFlightRequests.values()) {\n if (entry.abortTimer) clearTimeout(entry.abortTimer);\n }\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.getValid(key);\n return entry !== null;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
|
package/dist/connectors/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import "./jobs/index.js";
|
|
|
11
11
|
import { buildMcpHostPolicy } from "./mcp/host-policy.js";
|
|
12
12
|
import { AppKitMcpClient } from "./mcp/client.js";
|
|
13
13
|
import "./mcp/index.js";
|
|
14
|
-
import { SQLWarehouseConnector } from "./sql-warehouse/client.js";
|
|
14
|
+
import { DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS, SQLWarehouseConnector } from "./sql-warehouse/client.js";
|
|
15
15
|
import "./sql-warehouse/index.js";
|
|
16
16
|
import "./vector-search/index.js";
|
|
17
17
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
2
|
import { AppKitError } from "../../errors/base.js";
|
|
3
|
+
import { ConfigurationError } from "../../errors/configuration.js";
|
|
3
4
|
import { ConnectionError } from "../../errors/connection.js";
|
|
4
5
|
import { ExecutionError } from "../../errors/execution.js";
|
|
5
6
|
import { ValidationError } from "../../errors/validation.js";
|
|
@@ -8,14 +9,38 @@ import { TelemetryManager } from "../../telemetry/telemetry-manager.js";
|
|
|
8
9
|
import { SpanKind, SpanStatusCode } from "../../telemetry/index.js";
|
|
9
10
|
import { ArrowStreamProcessor } from "../../stream/arrow-stream-processor.js";
|
|
10
11
|
import { executeStatementDefaults } from "./defaults.js";
|
|
12
|
+
import { WarehousePollBackoff } from "./warehouse-poll-backoff.js";
|
|
13
|
+
import { WarehouseStatusEmitter } from "./warehouse-status-emitter.js";
|
|
11
14
|
import { Context } from "@databricks/sdk-experimental";
|
|
12
15
|
|
|
13
16
|
//#region src/connectors/sql-warehouse/client.ts
|
|
14
17
|
const logger = createLogger("connectors:sql-warehouse");
|
|
18
|
+
/**
|
|
19
|
+
* Default ceiling for how long {@link SQLWarehouseConnector.ensureWarehouseRunning}
|
|
20
|
+
* will wait for a warehouse to reach the RUNNING state before giving up.
|
|
21
|
+
*
|
|
22
|
+
* Five minutes covers a cold-start of a classic warehouse on most workspaces;
|
|
23
|
+
* serverless typically reaches RUNNING within ~30s.
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS = 300 * 1e3;
|
|
26
|
+
/**
|
|
27
|
+
* Window during which a recent `RUNNING` observation lets subsequent calls
|
|
28
|
+
* to {@link SQLWarehouseConnector.ensureWarehouseRunning} short-circuit
|
|
29
|
+
* without making any SDK calls. Sized to roughly the upper bound of how
|
|
30
|
+
* long Databricks keeps a warehouse "stickily" available between requests
|
|
31
|
+
* — past that, we re-verify.
|
|
32
|
+
*/
|
|
33
|
+
const WAREHOUSE_RUNNING_CACHE_TTL_MS = 3e4;
|
|
15
34
|
var SQLWarehouseConnector = class {
|
|
16
35
|
name = "sql-warehouse";
|
|
17
36
|
config;
|
|
18
37
|
_arrowProcessor = null;
|
|
38
|
+
/**
|
|
39
|
+
* Per-warehouse cache of the last RUNNING observation timestamp. Used by
|
|
40
|
+
* {@link ensureWarehouseRunning} to short-circuit warm-path callers; see
|
|
41
|
+
* {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}.
|
|
42
|
+
*/
|
|
43
|
+
_recentlyRunning = /* @__PURE__ */ new Map();
|
|
19
44
|
telemetry;
|
|
20
45
|
telemetryMetrics;
|
|
21
46
|
constructor(config) {
|
|
@@ -157,6 +182,148 @@ var SQLWarehouseConnector = class {
|
|
|
157
182
|
includePrefix: true
|
|
158
183
|
});
|
|
159
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Wait until the SQL warehouse is in the `RUNNING` state, auto-starting it
|
|
187
|
+
* if currently `STOPPED`. Emits a {@link WarehouseStatusUpdate} whenever
|
|
188
|
+
* the observed state changes, so callers can surface progress (e.g. over
|
|
189
|
+
* SSE) instead of letting the UI freeze on a cold warehouse. Equal
|
|
190
|
+
* successive observations are de-duplicated.
|
|
191
|
+
*
|
|
192
|
+
* Fast path: if this connector recently observed the warehouse RUNNING
|
|
193
|
+
* (within {@link WAREHOUSE_RUNNING_CACHE_TTL_MS}), the call returns
|
|
194
|
+
* immediately without any SDK round-trip or status emission. This keeps
|
|
195
|
+
* cache-hit analytics requests off the Databricks control plane.
|
|
196
|
+
*
|
|
197
|
+
* Behaviour by initial state:
|
|
198
|
+
* - `RUNNING`: emits one update, caches the observation, returns.
|
|
199
|
+
* - `STOPPED`: emits a synthetic `STARTING` update, calls
|
|
200
|
+
* `workspaceClient.warehouses.start`, then polls until `RUNNING`.
|
|
201
|
+
* When `autoStart: false`, throws `ConfigurationError` instead.
|
|
202
|
+
* - `STARTING` / `STOPPING`: polls until `RUNNING`.
|
|
203
|
+
* - `DELETED` / `DELETING`: throws `ConfigurationError.resourceNotFound`.
|
|
204
|
+
*
|
|
205
|
+
* Aborts and timeouts surface as `ExecutionError`. The poll loop uses
|
|
206
|
+
* exponential backoff with ±15% jitter (1s → 30s cap) and runs inside a
|
|
207
|
+
* `sql.warehouseReady` telemetry span.
|
|
208
|
+
*/
|
|
209
|
+
async ensureWarehouseRunning(workspaceClient, warehouseId, opts) {
|
|
210
|
+
const { onStatus, signal, timeoutMs = DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS, autoStart = true } = opts;
|
|
211
|
+
if (signal?.aborted) throw ExecutionError.canceled();
|
|
212
|
+
if (!warehouseId) throw ValidationError.missingField("warehouse_id");
|
|
213
|
+
if (this._isRecentlyRunning(warehouseId)) return;
|
|
214
|
+
return this.telemetry.startActiveSpan("sql.warehouseReady", {
|
|
215
|
+
kind: SpanKind.CLIENT,
|
|
216
|
+
attributes: {
|
|
217
|
+
"db.system": "databricks",
|
|
218
|
+
"db.warehouse_id": warehouseId,
|
|
219
|
+
"db.warehouse.startup_timeout_ms": timeoutMs
|
|
220
|
+
}
|
|
221
|
+
}, 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
|
+
try {
|
|
227
|
+
while (true) {
|
|
228
|
+
if (signal?.aborted) throw ExecutionError.canceled();
|
|
229
|
+
if (Date.now() - startTime > timeoutMs) throw ExecutionError.statementFailed(`SQL warehouse did not reach RUNNING within ${timeoutMs}ms`);
|
|
230
|
+
const info = await workspaceClient.warehouses.get({ id: warehouseId }, this._createContext(signal));
|
|
231
|
+
const state = info?.state;
|
|
232
|
+
const summary = info?.health?.summary;
|
|
233
|
+
switch (state) {
|
|
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"));
|
|
255
|
+
}
|
|
256
|
+
const sleepMs = backoff.next();
|
|
257
|
+
if (Date.now() + sleepMs - startTime >= timeoutMs) throw ExecutionError.statementFailed(`SQL warehouse did not reach RUNNING within ${timeoutMs}ms`);
|
|
258
|
+
await this._sleepRespectingAbort(sleepMs, signal);
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
this._throwSanitizedReadinessError(error, warehouseId, span);
|
|
262
|
+
} finally {
|
|
263
|
+
span.end();
|
|
264
|
+
}
|
|
265
|
+
}, {
|
|
266
|
+
name: this.name,
|
|
267
|
+
includePrefix: true
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* `true` if this connector observed `warehouseId` in the RUNNING state
|
|
272
|
+
* within the recent-cache TTL.
|
|
273
|
+
*/
|
|
274
|
+
_isRecentlyRunning(warehouseId) {
|
|
275
|
+
const observedAt = this._recentlyRunning.get(warehouseId);
|
|
276
|
+
return observedAt !== void 0 && Date.now() - observedAt < WAREHOUSE_RUNNING_CACHE_TTL_MS;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Final landing for any error thrown by the readiness poll loop. Records
|
|
280
|
+
* the original on the span and (at debug level) on the logger, then
|
|
281
|
+
* rethrows either the structured AppKitError unchanged or a curated
|
|
282
|
+
* ExecutionError — never the raw SDK message, which can contain operator
|
|
283
|
+
* internals.
|
|
284
|
+
*
|
|
285
|
+
* The `logger.debug` covers environments where OTel isn't configured
|
|
286
|
+
* (common in dev) so the raw error isn't lost just because no span
|
|
287
|
+
* exporter is hooked up.
|
|
288
|
+
*/
|
|
289
|
+
_throwSanitizedReadinessError(error, warehouseId, span) {
|
|
290
|
+
if (error instanceof AppKitError) {
|
|
291
|
+
span.recordException(error);
|
|
292
|
+
span.setStatus({
|
|
293
|
+
code: SpanStatusCode.ERROR,
|
|
294
|
+
message: error.code
|
|
295
|
+
});
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
299
|
+
logger.debug("Warehouse readiness check raw error for %s: %O", warehouseId, error);
|
|
300
|
+
const wrapped = ExecutionError.statementFailed("Warehouse readiness check failed");
|
|
301
|
+
span.setStatus({
|
|
302
|
+
code: SpanStatusCode.ERROR,
|
|
303
|
+
message: wrapped.code
|
|
304
|
+
});
|
|
305
|
+
throw wrapped;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Sleep for `ms` milliseconds, but resolve early (and reject with
|
|
309
|
+
* `ExecutionError.canceled()`) if `signal` aborts mid-sleep. Used by the
|
|
310
|
+
* warehouse readiness loop so that client disconnects don't keep the loop
|
|
311
|
+
* polling for the full interval.
|
|
312
|
+
*/
|
|
313
|
+
_sleepRespectingAbort(ms, signal) {
|
|
314
|
+
if (signal?.aborted) return Promise.reject(ExecutionError.canceled());
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
const timer = setTimeout(() => {
|
|
317
|
+
signal?.removeEventListener("abort", onAbort);
|
|
318
|
+
resolve();
|
|
319
|
+
}, ms);
|
|
320
|
+
const onAbort = () => {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
reject(ExecutionError.canceled());
|
|
323
|
+
};
|
|
324
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
325
|
+
});
|
|
326
|
+
}
|
|
160
327
|
async _pollForStatementResult(workspaceClient, statementId, timeout = executeStatementDefaults.timeout, signal) {
|
|
161
328
|
return this.telemetry.startActiveSpan("sql.poll", { attributes: {
|
|
162
329
|
"db.statement_id": statementId,
|
|
@@ -323,5 +490,5 @@ var SQLWarehouseConnector = class {
|
|
|
323
490
|
};
|
|
324
491
|
|
|
325
492
|
//#endregion
|
|
326
|
-
export { SQLWarehouseConnector };
|
|
493
|
+
export { DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS, SQLWarehouseConnector };
|
|
327
494
|
//# sourceMappingURL=client.js.map
|