@dbx-tools/shared 0.1.18
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/README.md +234 -0
- package/dist/index.client.d.ts +32 -0
- package/dist/index.client.js +32 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/src/api.d.ts +90 -0
- package/dist/src/api.js +165 -0
- package/dist/src/appkit.d.ts +59 -0
- package/dist/src/appkit.js +109 -0
- package/dist/src/common.d.ts +185 -0
- package/dist/src/common.js +277 -0
- package/dist/src/http.d.ts +77 -0
- package/dist/src/http.js +166 -0
- package/dist/src/log.d.ts +47 -0
- package/dist/src/log.js +80 -0
- package/dist/src/net.browser.d.ts +98 -0
- package/dist/src/net.browser.js +146 -0
- package/dist/src/net.d.ts +14 -0
- package/dist/src/net.js +29 -0
- package/dist/src/project.d.ts +33 -0
- package/dist/src/project.js +215 -0
- package/dist/src/string.d.ts +105 -0
- package/dist/src/string.js +220 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.client.ts +32 -0
- package/index.ts +26 -0
- package/package.json +54 -0
- package/src/api.ts +222 -0
- package/src/appkit.ts +161 -0
- package/src/common.ts +422 -0
- package/src/http.ts +203 -0
- package/src/log.ts +116 -0
- package/src/net.browser.ts +174 -0
- package/src/net.ts +32 -0
- package/src/project.ts +264 -0
- package/src/string.ts +276 -0
package/index.client.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe entry point for `@dbx-tools/shared`. Mirrors
|
|
3
|
+
* the server-side {@link ./index.ts} barrel except `projectUtils` is
|
|
4
|
+
* absent - it imports `node:fs` / `node:child_process` / `node:path` /
|
|
5
|
+
* `node:util` at module load, which Vite stubs for browsers and which
|
|
6
|
+
* blows up at the first property access from the stub.
|
|
7
|
+
*
|
|
8
|
+
* Resolution: the package's `exports` map points the `browser`
|
|
9
|
+
* condition at this file. Vite (and any other browser-aware bundler
|
|
10
|
+
* that honors `exports.<entry>.browser`) picks it up automatically;
|
|
11
|
+
* Node always uses `index.ts`. Don't import `./src/project.js` from
|
|
12
|
+
* here, even transitively - that's the entire point of the split.
|
|
13
|
+
*
|
|
14
|
+
* Other utility namespaces are re-exported as-is. `common.ts` ships a
|
|
15
|
+
* pure-JS FNV-1a `fnvHash` (no `node:crypto`) that `string.ts` uses
|
|
16
|
+
* for slug suffixes, so the whole barrel is safe in the browser;
|
|
17
|
+
* `http.ts` / `log.ts` already had no node-only imports.
|
|
18
|
+
*
|
|
19
|
+
* `apiUtils` and `appkitUtils` are intentionally **not** re-exported
|
|
20
|
+
* here. Both import from `@databricks/appkit`, whose main barrel
|
|
21
|
+
* re-exports server-only typegen helpers
|
|
22
|
+
* (`extractServingEndpoints`, the `appKit*TypesPlugin` Vite plugins)
|
|
23
|
+
* that transitively load `@ast-grep/napi`'s native `.node` binary.
|
|
24
|
+
* Letting either land in the browser bundle drags the entire appkit
|
|
25
|
+
* tree (including ast-grep) into the client. They live only on
|
|
26
|
+
* `index.ts` (the server entry).
|
|
27
|
+
*/
|
|
28
|
+
export * as commonUtils from "./src/common.js";
|
|
29
|
+
export * as httpUtils from "./src/http.js";
|
|
30
|
+
export * as logUtils from "./src/log.js";
|
|
31
|
+
export * as netUtils from "./src/net.browser.js";
|
|
32
|
+
export * as stringUtils from "./src/string.js";
|
package/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side entry point for `@dbx-tools/shared`. Re-exports
|
|
3
|
+
* everything from the browser-safe {@link ./index.client.ts} barrel
|
|
4
|
+
* and adds the server-only namespaces:
|
|
5
|
+
* - `projectUtils` imports `node:fs` / `node:child_process` /
|
|
6
|
+
* `node:path` / `node:util`.
|
|
7
|
+
* - `apiUtils` / `appkitUtils` both import from `@databricks/appkit`,
|
|
8
|
+
* whose barrel transitively pulls in the typegen helpers and the
|
|
9
|
+
* `@ast-grep/napi` native binary. Keeping them out of the
|
|
10
|
+
* browser entry stops that whole subtree from being bundled
|
|
11
|
+
* for the client.
|
|
12
|
+
*
|
|
13
|
+
* Resolution: this file is the `import` / `default` target in the
|
|
14
|
+
* package's `exports` map. Vite (and any other browser-aware
|
|
15
|
+
* bundler that honors `exports.<entry>.browser`) picks
|
|
16
|
+
* `index.client.ts` instead, so the node-only branches never ship
|
|
17
|
+
* to the client. Add new browser-safe helpers to `index.client.ts`
|
|
18
|
+
* to keep this file as the thin server-only delta.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export * as appkitUtils from "./src/appkit.js";
|
|
22
|
+
export * as apiUtils from "./src/api.js";
|
|
23
|
+
export * as netUtils from "./src/net.js";
|
|
24
|
+
export * as projectUtils from "./src/project.js";
|
|
25
|
+
|
|
26
|
+
export * from "./index.client.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"main": "./dist/index.js",
|
|
3
|
+
"types": "./dist/index.d.ts",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"browser": {
|
|
7
|
+
"types": "./dist/index.client.d.ts",
|
|
8
|
+
"default": "./dist/index.client.js"
|
|
9
|
+
},
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"source": {
|
|
15
|
+
"browser": "./index.client.ts",
|
|
16
|
+
"default": "./index.ts"
|
|
17
|
+
},
|
|
18
|
+
"default": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"name": "@dbx-tools/shared",
|
|
25
|
+
"version": "0.1.18",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"fast-deep-equal": "^3.1.3",
|
|
28
|
+
"lru-cache": "^11.5.1",
|
|
29
|
+
"zod": "^4.3.6"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@databricks/sdk-experimental": "^0.17"
|
|
33
|
+
},
|
|
34
|
+
"module": "index.ts",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@databricks/appkit": "*"
|
|
37
|
+
},
|
|
38
|
+
"type": "module",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"index*.ts",
|
|
42
|
+
"src"
|
|
43
|
+
],
|
|
44
|
+
"license": "Apache-2.0",
|
|
45
|
+
"homepage": "https://github.com/reggie-db/dbx-tools-appkit#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/reggie-db/dbx-tools-appkit/issues"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/reggie-db/dbx-tools-appkit.git",
|
|
52
|
+
"directory": "packages/shared"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { CancellationToken, WorkspaceClient } from "@databricks/sdk-experimental";
|
|
2
|
+
import { Context } from "@databricks/sdk-experimental";
|
|
3
|
+
import { CacheManager, getExecutionContext } from "@databricks/appkit";
|
|
4
|
+
|
|
5
|
+
// Direct imports (not via the barrel). The package's NodeNext
|
|
6
|
+
// module resolution wants explicit `.js` extensions on relative
|
|
7
|
+
// imports, and reaching for `commonUtils` / `netUtils` through
|
|
8
|
+
// `../index.client` confused the `noEmit` typecheck with a
|
|
9
|
+
// missing-extension error. Direct sibling imports stay typed and
|
|
10
|
+
// don't risk a future cycle.
|
|
11
|
+
import { fnvHash, tieAbortSignal } from "./common.js";
|
|
12
|
+
import { joinUrl, parseUrl } from "./net.browser.js";
|
|
13
|
+
|
|
14
|
+
// ────────────────────────────────────────────────────────────────
|
|
15
|
+
// Constants
|
|
16
|
+
// ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const API_PREFIX = "/api/2.0";
|
|
19
|
+
type GetOrExecuteParams = Parameters<CacheManager["getOrExecute"]>;
|
|
20
|
+
type ApiRequestInit = RequestInit & {
|
|
21
|
+
cache?: {
|
|
22
|
+
key?: GetOrExecuteParams[0];
|
|
23
|
+
userKey?: GetOrExecuteParams[2];
|
|
24
|
+
options: GetOrExecuteParams[3];
|
|
25
|
+
};
|
|
26
|
+
workspaceClient?: WorkspaceClient;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the absolute `URL` for a Databricks workspace REST endpoint
|
|
31
|
+
* without issuing a request. Mirrors {@link fetchApi}'s path handling
|
|
32
|
+
* (single string or array of segments, leading `/api/2.0` stripped)
|
|
33
|
+
* so callers can construct request URLs that match what `fetchApi`
|
|
34
|
+
* would have used. Resolves the host from the supplied
|
|
35
|
+
* `WorkspaceClient` or, when omitted, from the active
|
|
36
|
+
* `getExecutionContext().client`.
|
|
37
|
+
*/
|
|
38
|
+
export async function apiUrl(
|
|
39
|
+
path: string[] | string,
|
|
40
|
+
workspaceClient?: WorkspaceClient,
|
|
41
|
+
): Promise<URL> {
|
|
42
|
+
let joinedPath = joinUrl(path);
|
|
43
|
+
if (joinedPath === API_PREFIX || joinedPath.startsWith(API_PREFIX + "/")) {
|
|
44
|
+
joinedPath = joinedPath.slice(API_PREFIX.length);
|
|
45
|
+
}
|
|
46
|
+
if (!joinedPath) {
|
|
47
|
+
throw new Error(`Invalid path: ${path}`);
|
|
48
|
+
}
|
|
49
|
+
const client = workspaceClient ?? getExecutionContext().client;
|
|
50
|
+
const config = client.config;
|
|
51
|
+
const host = await config.getHost();
|
|
52
|
+
const url = parseUrl(host, API_PREFIX, joinedPath)!;
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Issue an authenticated request against a Databricks workspace REST
|
|
58
|
+
* endpoint, resolving the host from the supplied `WorkspaceClient`
|
|
59
|
+
* and stamping the OAuth/PAT auth header in for you. The response
|
|
60
|
+
* body is returned parsed as JSON.
|
|
61
|
+
*
|
|
62
|
+
* `path` may be a single string or an array of segments. A leading
|
|
63
|
+
* `/api/2.0` is auto-stripped so callers can pass either style
|
|
64
|
+
* (`"/api/2.0/serving-endpoints"` or `"/serving-endpoints"`) without
|
|
65
|
+
* doubling it in the final URL.
|
|
66
|
+
*
|
|
67
|
+
* `init` is an optional WHATWG `RequestInit`. Useful fields:
|
|
68
|
+
*
|
|
69
|
+
* - `body`: request payload. Strings / `Buffer` / `FormData` /
|
|
70
|
+
* `URLSearchParams` pass through; for JSON, stringify the object
|
|
71
|
+
* yourself and set `headers["Content-Type"] = "application/json"`.
|
|
72
|
+
* - `headers`: extra request headers, merged in **before** the auth
|
|
73
|
+
* header is applied so the workspace's `Authorization` always
|
|
74
|
+
* wins on conflict.
|
|
75
|
+
* - `method`: HTTP verb. If omitted, defaults to `POST` when
|
|
76
|
+
* `init.body` is present and `GET` otherwise.
|
|
77
|
+
*
|
|
78
|
+
* `cache` is an optional handle to `CacheManager.getOrExecute`: pass
|
|
79
|
+
* `{ options: { ttl: 300 } }` for a per-user, time-boxed cache; the
|
|
80
|
+
* `userId` from the active execution context becomes part of the
|
|
81
|
+
* cache key by default.
|
|
82
|
+
*
|
|
83
|
+
* `workspaceClient` is optional; when omitted the request uses the
|
|
84
|
+
* caller's `getExecutionContext().client` (i.e. the per-request
|
|
85
|
+
* OBO client). Pass an explicit client for service-account work
|
|
86
|
+
* outside a request.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* await fetchApi("/serving-endpoints");
|
|
90
|
+
*
|
|
91
|
+
* await fetchApi(["/serving-endpoints", endpointName, "invocations"], {
|
|
92
|
+
* body: JSON.stringify({ inputs: [...] }),
|
|
93
|
+
* headers: { "Content-Type": "application/json" },
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* await fetchApi("/serving-endpoints", undefined,
|
|
97
|
+
* { options: { ttl: 300 } }
|
|
98
|
+
* );
|
|
99
|
+
*/
|
|
100
|
+
export async function fetchApi<T>(
|
|
101
|
+
target: URL | string[] | string,
|
|
102
|
+
init?: ApiRequestInit,
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
const client = init?.workspaceClient ?? getExecutionContext().client;
|
|
105
|
+
const config = client.config;
|
|
106
|
+
const url = target instanceof URL ? target : await apiUrl(target, client);
|
|
107
|
+
if (init?.cache) {
|
|
108
|
+
const { cache, ...executeInit } = init;
|
|
109
|
+
const executionContext = getExecutionContext();
|
|
110
|
+
const userId =
|
|
111
|
+
"userId" in executionContext
|
|
112
|
+
? executionContext.userId
|
|
113
|
+
: executionContext.serviceUserId;
|
|
114
|
+
const cacheInstance = await CacheManager.getInstance();
|
|
115
|
+
|
|
116
|
+
return cacheInstance.getOrExecute(
|
|
117
|
+
cache.key ?? ["fetchApi", userId],
|
|
118
|
+
async () => {
|
|
119
|
+
return fetchApi(url, { ...executeInit, workspaceClient: client });
|
|
120
|
+
},
|
|
121
|
+
cache.userKey ?? fnvHash(url.toString()),
|
|
122
|
+
cache.options,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const headers = new Headers(init?.headers);
|
|
126
|
+
await config.authenticate(headers);
|
|
127
|
+
const method = init?.method?.toUpperCase() ?? (init?.body ? "POST" : "GET");
|
|
128
|
+
const response = await fetch(url.toString(), {
|
|
129
|
+
...init,
|
|
130
|
+
method,
|
|
131
|
+
headers,
|
|
132
|
+
});
|
|
133
|
+
return response.json() as Promise<T>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type ContextLike = Context | AbortSignal;
|
|
137
|
+
|
|
138
|
+
/** Wrap a `Context` (returned as-is) or `AbortSignal` (adapted) as an SDK `Context`. */
|
|
139
|
+
export function toContext(input: ContextLike): Context;
|
|
140
|
+
/**
|
|
141
|
+
* Derive an SDK `Context` from `controller.signal`, optionally tying
|
|
142
|
+
* `input` into the controller so the controller becomes the single
|
|
143
|
+
* cancellation source for downstream SDK calls:
|
|
144
|
+
*
|
|
145
|
+
* - `AbortSignal`: aborting it propagates into `controller` (and from
|
|
146
|
+
* there into every SDK call you pass the returned context to).
|
|
147
|
+
* - `Context`: its `cancellationToken` is tied into `controller`, and
|
|
148
|
+
* its other fields (`logger`, `opName`, `rootClassName`,
|
|
149
|
+
* `rootFnName`, `opId`) are preserved in the returned `Context`.
|
|
150
|
+
* The returned context's `cancellationToken` is replaced with one
|
|
151
|
+
* backed by `controller.signal`.
|
|
152
|
+
*
|
|
153
|
+
* The tie is one-way (parent -> child): aborting `controller`
|
|
154
|
+
* directly does NOT cancel `input`. So a request-level cancel (your
|
|
155
|
+
* loop's `try/finally { controller.abort() }`) won't tear down a
|
|
156
|
+
* caller-supplied AbortSignal it didn't own.
|
|
157
|
+
*/
|
|
158
|
+
export function toContext(controller: AbortController, input?: ContextLike): Context;
|
|
159
|
+
export function toContext(
|
|
160
|
+
source: AbortController | ContextLike,
|
|
161
|
+
input?: ContextLike,
|
|
162
|
+
): Context {
|
|
163
|
+
if (!(source instanceof AbortController)) {
|
|
164
|
+
if (source instanceof Context) return source;
|
|
165
|
+
return new Context({ cancellationToken: signalToCancellationToken(source) });
|
|
166
|
+
}
|
|
167
|
+
if (input instanceof AbortSignal) {
|
|
168
|
+
tieAbortSignal(source, input);
|
|
169
|
+
} else if (input instanceof Context) {
|
|
170
|
+
const token = input.cancellationToken;
|
|
171
|
+
if (token) tieCancellationToken(source, token);
|
|
172
|
+
const merged = input.copy();
|
|
173
|
+
merged.setItems({ cancellationToken: signalToCancellationToken(source.signal) });
|
|
174
|
+
return merged;
|
|
175
|
+
}
|
|
176
|
+
return new Context({ cancellationToken: signalToCancellationToken(source.signal) });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Adapt a WHATWG `AbortSignal` to the Databricks SDK's
|
|
181
|
+
* `CancellationToken` interface. The SDK's `api-client.ts`
|
|
182
|
+
* internally creates an `AbortController` and wires
|
|
183
|
+
* `cancellationToken.onCancellationRequested` to it, so this
|
|
184
|
+
* adapter is the one-line bridge from "platform-standard
|
|
185
|
+
* cancellation" to "the SDK aborts the fetch on your behalf".
|
|
186
|
+
*
|
|
187
|
+
* Kept private for now: the genie package is the only consumer in
|
|
188
|
+
* the workspace. Lift to `@dbx-tools/shared` (`apiUtils`) the
|
|
189
|
+
* moment a second package needs SDK-call cancellation.
|
|
190
|
+
*/
|
|
191
|
+
function signalToCancellationToken(signal: AbortSignal): CancellationToken {
|
|
192
|
+
return {
|
|
193
|
+
get isCancellationRequested() {
|
|
194
|
+
return signal.aborted;
|
|
195
|
+
},
|
|
196
|
+
onCancellationRequested(cb) {
|
|
197
|
+
if (signal.aborted) {
|
|
198
|
+
cb(signal.reason);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
signal.addEventListener("abort", () => cb(signal.reason), { once: true });
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Tie the SDK's `CancellationToken` interface back into an
|
|
208
|
+
* `AbortController`. Mirrors {@link tieAbortSignal} but for the
|
|
209
|
+
* SDK's cancellation shape, used when a caller hands us a
|
|
210
|
+
* pre-built `Context` whose token we want to fold into our own
|
|
211
|
+
* controller.
|
|
212
|
+
*/
|
|
213
|
+
function tieCancellationToken(
|
|
214
|
+
controller: AbortController,
|
|
215
|
+
token: CancellationToken,
|
|
216
|
+
): void {
|
|
217
|
+
if (token.isCancellationRequested) {
|
|
218
|
+
controller.abort();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
token.onCancellationRequested((reason) => controller.abort(reason));
|
|
222
|
+
}
|
package/src/appkit.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Helpers for working with the AppKit plugin context (`this.context` on
|
|
2
|
+
// any class that extends `Plugin` from `@databricks/appkit`).
|
|
3
|
+
//
|
|
4
|
+
// Why these live here instead of in `@databricks/appkit`: AppKit exposes
|
|
5
|
+
// `this.context.getPlugins()`, which returns
|
|
6
|
+
// `ReadonlyMap<string, BasePlugin>`, but provides no typed lookup
|
|
7
|
+
// helper. Every caller ends up writing the same
|
|
8
|
+
// `as InstanceType<ReturnType<typeof someFactory>["plugin"]>` cast.
|
|
9
|
+
// These wrappers absorb that boilerplate.
|
|
10
|
+
//
|
|
11
|
+
// API shape: pass the plugin's factory (`lakebase`, `serving`, `genie`,
|
|
12
|
+
// or any `toPlugin(...)` result) directly. TypeScript infers both the
|
|
13
|
+
// instance type (so `.exports()` resolves) and the registered name (so
|
|
14
|
+
// the runtime lookup works) from that single value. No `<T>` annotation
|
|
15
|
+
// or string literal needed at the call site.
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
CacheManager,
|
|
19
|
+
createApp,
|
|
20
|
+
getExecutionContext,
|
|
21
|
+
InitializationError,
|
|
22
|
+
} from "@databricks/appkit";
|
|
23
|
+
import type { NameLike } from "./common.js";
|
|
24
|
+
import { memoize } from "./common.js";
|
|
25
|
+
|
|
26
|
+
// Minimal structural shape of `this.context`. We mirror only the method
|
|
27
|
+
// we touch instead of depending on AppKit's `PluginContext` type, which
|
|
28
|
+
// is not part of the package's `exports` map and therefore cannot be
|
|
29
|
+
// imported. Any compatible object (real `PluginContext`, mocks, tests)
|
|
30
|
+
// satisfies this shape.
|
|
31
|
+
export interface PluginContextLike {
|
|
32
|
+
getPlugins(): ReadonlyMap<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type PluginData = {
|
|
36
|
+
plugin: abstract new (...args: never[]) => unknown;
|
|
37
|
+
name: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Structural shape of an AppKit plugin factory (the result of
|
|
41
|
+
// `toPlugin(SomePluginClass)`). Calling it returns a `PluginData` tuple
|
|
42
|
+
// whose `plugin` field is the *class constructor* and whose `name`
|
|
43
|
+
// field carries the registered plugin name as a literal string.
|
|
44
|
+
//
|
|
45
|
+
// Defined structurally so we don't pull `@databricks/appkit` into this
|
|
46
|
+
// package as a runtime or type dependency. Any function returning the
|
|
47
|
+
// same shape (e.g. `lakebase`, `serving`, `genie`, or a user-defined
|
|
48
|
+
// `toPlugin(MyPlugin)`) satisfies the bound.
|
|
49
|
+
type PluginDataFactory = (...args: never[]) => PluginData;
|
|
50
|
+
|
|
51
|
+
// Maps a plugin factory back to the *instance* type of its plugin
|
|
52
|
+
// class. Mirrors the inline pattern users would otherwise write:
|
|
53
|
+
// `InstanceType<ReturnType<typeof factory>["plugin"]>`.
|
|
54
|
+
type PluginInstanceOf<F extends PluginDataFactory> = InstanceType<
|
|
55
|
+
ReturnType<F>["plugin"]
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
// Registry name returned by `factory().name`, keyed by the factory
|
|
59
|
+
// function. Typical AppKit factories return stable metadata; caching
|
|
60
|
+
// avoids invoking `factory()` on every sibling lookup (which would
|
|
61
|
+
// allocate a fresh descriptor tuple each time).
|
|
62
|
+
const dataCache = new WeakMap<PluginDataFactory, PluginData>();
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the static `{ plugin, name }` descriptor for an AppKit plugin
|
|
66
|
+
* factory, caching per factory so repeated lookups do not allocate.
|
|
67
|
+
*/
|
|
68
|
+
export function data<F extends PluginDataFactory, D extends ReturnType<F>>(
|
|
69
|
+
factory: F,
|
|
70
|
+
): D {
|
|
71
|
+
const cached = dataCache.get(factory);
|
|
72
|
+
if (cached !== undefined) {
|
|
73
|
+
return cached as D;
|
|
74
|
+
}
|
|
75
|
+
const result = factory();
|
|
76
|
+
dataCache.set(factory, result);
|
|
77
|
+
return result as D;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Look up a sibling plugin instance from the AppKit plugin context,
|
|
82
|
+
* keyed off the factory's registered name and typed via its plugin
|
|
83
|
+
* class.
|
|
84
|
+
*
|
|
85
|
+
* Returns `undefined` when the context is missing or the plugin is not
|
|
86
|
+
* registered. For required siblings prefer {@link require}.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { lakebase } from "@databricks/appkit";
|
|
91
|
+
* import { appkitUtils } from "@dbx-tools/shared";
|
|
92
|
+
*
|
|
93
|
+
* const lake = appkitUtils.instance(this.context, lakebase);
|
|
94
|
+
* // ^^ inferred as LakebasePlugin | undefined
|
|
95
|
+
* lake?.exports().pool;
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function instance<F extends PluginDataFactory>(
|
|
99
|
+
ctx: PluginContextLike | undefined,
|
|
100
|
+
factory: F,
|
|
101
|
+
): PluginInstanceOf<F> | undefined {
|
|
102
|
+
if (!ctx) return undefined;
|
|
103
|
+
const name = data(factory).name;
|
|
104
|
+
return ctx.getPlugins().get(name) as PluginInstanceOf<F> | undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Like {@link instance} but throws when the plugin is not registered.
|
|
109
|
+
* Use for siblings whose absence is a wiring bug rather than a runtime
|
|
110
|
+
* condition (e.g. requiring `lakebase` when the caller has `storage` /
|
|
111
|
+
* `memory` enabled).
|
|
112
|
+
*
|
|
113
|
+
* `caller` is prepended to the error message so cross-plugin failures
|
|
114
|
+
* are easy to attribute in logs.
|
|
115
|
+
*
|
|
116
|
+
* Always accessed through the namespace as `appkitUtils.require(...)`;
|
|
117
|
+
* the bare identifier is legal here because this package is pure ESM.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { lakebase } from "@databricks/appkit";
|
|
122
|
+
* import { appkitUtils } from "@dbx-tools/shared";
|
|
123
|
+
*
|
|
124
|
+
* const pool = appkitUtils.require(this.context, lakebase, "mastra")
|
|
125
|
+
* .exports().pool;
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function require<F extends PluginDataFactory>(
|
|
129
|
+
ctx: PluginContextLike | undefined,
|
|
130
|
+
factory: F,
|
|
131
|
+
caller?: NameLike | string,
|
|
132
|
+
): PluginInstanceOf<F> {
|
|
133
|
+
const plugin = instance(ctx, factory);
|
|
134
|
+
if (plugin) return plugin;
|
|
135
|
+
const prefix =
|
|
136
|
+
typeof caller === "string" ? `${caller}: ` : caller?.name ? `${caller.name}: ` : "";
|
|
137
|
+
const registeredName = data(factory).name;
|
|
138
|
+
throw new Error(`${prefix}required plugin not registered: ${registeredName}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isInitialized(): boolean {
|
|
142
|
+
try {
|
|
143
|
+
const ctx = getExecutionContext();
|
|
144
|
+
if (ctx?.client) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!(error instanceof InitializationError)) {
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function ensureInitialized() {
|
|
156
|
+
if (!isInitialized()) {
|
|
157
|
+
await createApp({
|
|
158
|
+
plugins: [],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|