@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
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
import { CacheManager, createApp, getExecutionContext, InitializationError, } from "@databricks/appkit";
|
|
17
|
+
import { memoize } from "./common.js";
|
|
18
|
+
// Registry name returned by `factory().name`, keyed by the factory
|
|
19
|
+
// function. Typical AppKit factories return stable metadata; caching
|
|
20
|
+
// avoids invoking `factory()` on every sibling lookup (which would
|
|
21
|
+
// allocate a fresh descriptor tuple each time).
|
|
22
|
+
const dataCache = new WeakMap();
|
|
23
|
+
/**
|
|
24
|
+
* Returns the static `{ plugin, name }` descriptor for an AppKit plugin
|
|
25
|
+
* factory, caching per factory so repeated lookups do not allocate.
|
|
26
|
+
*/
|
|
27
|
+
export function data(factory) {
|
|
28
|
+
const cached = dataCache.get(factory);
|
|
29
|
+
if (cached !== undefined) {
|
|
30
|
+
return cached;
|
|
31
|
+
}
|
|
32
|
+
const result = factory();
|
|
33
|
+
dataCache.set(factory, result);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Look up a sibling plugin instance from the AppKit plugin context,
|
|
38
|
+
* keyed off the factory's registered name and typed via its plugin
|
|
39
|
+
* class.
|
|
40
|
+
*
|
|
41
|
+
* Returns `undefined` when the context is missing or the plugin is not
|
|
42
|
+
* registered. For required siblings prefer {@link require}.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { lakebase } from "@databricks/appkit";
|
|
47
|
+
* import { appkitUtils } from "@dbx-tools/shared";
|
|
48
|
+
*
|
|
49
|
+
* const lake = appkitUtils.instance(this.context, lakebase);
|
|
50
|
+
* // ^^ inferred as LakebasePlugin | undefined
|
|
51
|
+
* lake?.exports().pool;
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function instance(ctx, factory) {
|
|
55
|
+
if (!ctx)
|
|
56
|
+
return undefined;
|
|
57
|
+
const name = data(factory).name;
|
|
58
|
+
return ctx.getPlugins().get(name);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Like {@link instance} but throws when the plugin is not registered.
|
|
62
|
+
* Use for siblings whose absence is a wiring bug rather than a runtime
|
|
63
|
+
* condition (e.g. requiring `lakebase` when the caller has `storage` /
|
|
64
|
+
* `memory` enabled).
|
|
65
|
+
*
|
|
66
|
+
* `caller` is prepended to the error message so cross-plugin failures
|
|
67
|
+
* are easy to attribute in logs.
|
|
68
|
+
*
|
|
69
|
+
* Always accessed through the namespace as `appkitUtils.require(...)`;
|
|
70
|
+
* the bare identifier is legal here because this package is pure ESM.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { lakebase } from "@databricks/appkit";
|
|
75
|
+
* import { appkitUtils } from "@dbx-tools/shared";
|
|
76
|
+
*
|
|
77
|
+
* const pool = appkitUtils.require(this.context, lakebase, "mastra")
|
|
78
|
+
* .exports().pool;
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function require(ctx, factory, caller) {
|
|
82
|
+
const plugin = instance(ctx, factory);
|
|
83
|
+
if (plugin)
|
|
84
|
+
return plugin;
|
|
85
|
+
const prefix = typeof caller === "string" ? `${caller}: ` : caller?.name ? `${caller.name}: ` : "";
|
|
86
|
+
const registeredName = data(factory).name;
|
|
87
|
+
throw new Error(`${prefix}required plugin not registered: ${registeredName}`);
|
|
88
|
+
}
|
|
89
|
+
export function isInitialized() {
|
|
90
|
+
try {
|
|
91
|
+
const ctx = getExecutionContext();
|
|
92
|
+
if (ctx?.client) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!(error instanceof InitializationError)) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
export async function ensureInitialized() {
|
|
104
|
+
if (!isInitialized()) {
|
|
105
|
+
await createApp({
|
|
106
|
+
plugins: [],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** Minimal shape for objects that expose an optional `name` (e.g. AppKit plugins). */
|
|
2
|
+
export interface NameLike {
|
|
3
|
+
name?: string;
|
|
4
|
+
}
|
|
5
|
+
type MemoizeKeyFn<TArgs extends readonly unknown[]> = (...args: TArgs) => string;
|
|
6
|
+
export interface MemoizeOptions<TArgs extends readonly unknown[]> {
|
|
7
|
+
/** Build a cache key from call arguments. Defaults to `JSON.stringify(args)`. */
|
|
8
|
+
key?: MemoizeKeyFn<TArgs>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run a zero-argument factory once; later calls return the same result.
|
|
12
|
+
*
|
|
13
|
+
* Concurrent callers share one in-flight promise until the factory settles.
|
|
14
|
+
* Thenable returns (anything with a `.then` method) are accepted; the
|
|
15
|
+
* cached value is always a native `Promise<T>` because we route through
|
|
16
|
+
* `Promise.resolve().then(factory)`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function memoize<T>(factory: () => T | PromiseLike<T>): () => Promise<T>;
|
|
19
|
+
/**
|
|
20
|
+
* Memoize by call arguments. Sync `fn` returns values directly; if `fn`
|
|
21
|
+
* returns a thenable (`Promise` or any object with a `.then` method),
|
|
22
|
+
* concurrent calls for the same key share one in-flight promise.
|
|
23
|
+
*
|
|
24
|
+
* Input is `T | PromiseLike<T>` so foreign thenables (e.g. third-party
|
|
25
|
+
* promise libraries, hand-rolled `{ then }` shims) are accepted; the
|
|
26
|
+
* async branch wraps them with `Promise.resolve(...)` so the cached
|
|
27
|
+
* entry is always a native `Promise<T>` even when the caller hands us a
|
|
28
|
+
* non-spec-compliant thenable.
|
|
29
|
+
*/
|
|
30
|
+
export declare function memoize<TArgs extends readonly unknown[], TReturn>(fn: (...args: TArgs) => TReturn | PromiseLike<TReturn>, options?: MemoizeOptions<TArgs>): (...args: TArgs) => TReturn | Promise<TReturn>;
|
|
31
|
+
/**
|
|
32
|
+
* Method decorator: memoizes the decorated method by its arguments.
|
|
33
|
+
*
|
|
34
|
+
* Requires `experimentalDecorators` in the consumer's `tsconfig.json`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function memoized(_target: object, _propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor;
|
|
37
|
+
/**
|
|
38
|
+
* Per-iteration context handed to {@link PollProducer} and the
|
|
39
|
+
* predicate on each step of a {@link poll} loop. Bundles the
|
|
40
|
+
* iteration metadata so the call signatures stay stable as `poll`
|
|
41
|
+
* grows additional fields.
|
|
42
|
+
*
|
|
43
|
+
* `signal` is owned by `poll`: it tracks the external
|
|
44
|
+
* `PollOptions.signal` (when supplied) and also fires when the
|
|
45
|
+
* consumer breaks out of the loop, so producers can forward it to
|
|
46
|
+
* any in-flight work (`fetch`, SDK calls, etc.) and have a single
|
|
47
|
+
* cancellation source tear down both the request and the loop.
|
|
48
|
+
*
|
|
49
|
+
* `attributes` is a mutable scratchpad shared across every
|
|
50
|
+
* iteration of a single `poll` run. The same object reference is
|
|
51
|
+
* passed each call so writes from one iteration are visible to the
|
|
52
|
+
* next - useful for stashing per-loop state (retry counters, start
|
|
53
|
+
* timestamps, anything you'd otherwise close over via a let).
|
|
54
|
+
* Generic `A` lets callers type the bag; defaults to
|
|
55
|
+
* `Record<string, unknown>`.
|
|
56
|
+
*/
|
|
57
|
+
export interface PollContext<T, A = Record<string, unknown>> {
|
|
58
|
+
/** Zero-based iteration index (`0` on the first call). */
|
|
59
|
+
attempt: number;
|
|
60
|
+
/** Value yielded on the prior iteration; `undefined` on the first. */
|
|
61
|
+
previous: T | undefined;
|
|
62
|
+
/** Cancellation handle. Always defined; forward to in-flight work. */
|
|
63
|
+
signal: AbortSignal;
|
|
64
|
+
/** Per-run mutable scratchpad shared across iterations. */
|
|
65
|
+
attributes: A;
|
|
66
|
+
}
|
|
67
|
+
/** One step of a {@link poll} loop. See {@link PollContext}. */
|
|
68
|
+
export type PollProducer<T, A = Record<string, unknown>> = (ctx: PollContext<T, A>) => T | PromiseLike<T>;
|
|
69
|
+
export interface PollOptions<T, A = Record<string, unknown>> {
|
|
70
|
+
/** Milliseconds to wait between polls. */
|
|
71
|
+
intervalMs: number;
|
|
72
|
+
/**
|
|
73
|
+
* Predicate evaluated against each yielded value: return `true` to
|
|
74
|
+
* keep polling, `false` to stop. May be sync or async - a
|
|
75
|
+
* `PromiseLike<boolean>` is awaited before the decision is made.
|
|
76
|
+
* Receives the same {@link PollContext} as the producer (same
|
|
77
|
+
* `signal`, same `attributes` bag), so an async predicate can
|
|
78
|
+
* forward the signal to its own in-flight work or read/write
|
|
79
|
+
* shared state.
|
|
80
|
+
*
|
|
81
|
+
* Omit to poll forever (the consumer stops by breaking out of the
|
|
82
|
+
* loop or by aborting `signal`).
|
|
83
|
+
*/
|
|
84
|
+
filter?: ((value: T, ctx: PollContext<T, A>) => boolean | PromiseLike<boolean>) | "distinct";
|
|
85
|
+
predicate?: (value: T, ctx: PollContext<T, A>) => boolean | PromiseLike<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* External cancellation handle. Tied into the internal signal that
|
|
88
|
+
* `poll` hands to `producer`, so aborting it tears down both the
|
|
89
|
+
* in-flight request and the inter-poll sleep.
|
|
90
|
+
*/
|
|
91
|
+
signal?: AbortSignal;
|
|
92
|
+
/**
|
|
93
|
+
* Initial value for `ctx.attributes`. Defaults to `{}`. The same
|
|
94
|
+
* object is reused across iterations, so callers can pre-populate
|
|
95
|
+
* fields (timers, retry counters, etc.) and the producer /
|
|
96
|
+
* predicate can mutate them in place.
|
|
97
|
+
*/
|
|
98
|
+
attributes?: A;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Async iterable that drives a periodic poll. Each iteration:
|
|
102
|
+
*
|
|
103
|
+
* 1. Builds a {@link PollContext} (`attempt`, `previous`, `signal`,
|
|
104
|
+
* shared `attributes`) and calls `producer(ctx)`; yields the
|
|
105
|
+
* resolved value.
|
|
106
|
+
* 2. Evaluates `options.predicate(value, ctx)`; stops when it
|
|
107
|
+
* returns (or resolves to) `false`.
|
|
108
|
+
* 3. Sleeps `options.intervalMs` before the next attempt.
|
|
109
|
+
*
|
|
110
|
+
* The first call runs immediately (no leading sleep) so the consumer
|
|
111
|
+
* sees a value without waiting an interval. Errors thrown by
|
|
112
|
+
* `producer` propagate through the generator.
|
|
113
|
+
*
|
|
114
|
+
* `poll` always creates an internal `AbortController` and exposes
|
|
115
|
+
* `internal.signal` as `ctx.signal`, so producers can rely on a
|
|
116
|
+
* defined signal without a nullish check. The external
|
|
117
|
+
* `options.signal` is tied in, and a `try/finally` aborts the
|
|
118
|
+
* internal signal when the consumer breaks out of the `for await`
|
|
119
|
+
* (or the loop throws), so any producer work still holding the
|
|
120
|
+
* signal sees the cancellation too.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* for await (const msg of poll(
|
|
124
|
+
* async ({ signal }) =>
|
|
125
|
+
* client.genie.getMessage({ ... }, { abortSignal: signal }),
|
|
126
|
+
* {
|
|
127
|
+
* intervalMs: 250,
|
|
128
|
+
* predicate: (m) => !TERMINAL_STATUSES.has(m.status),
|
|
129
|
+
* signal: controller.signal,
|
|
130
|
+
* },
|
|
131
|
+
* )) {
|
|
132
|
+
* render(msg);
|
|
133
|
+
* }
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* // Typed attributes for per-run state.
|
|
137
|
+
* type Stats = { failures: number; startedAt: number };
|
|
138
|
+
* for await (const x of poll<Thing, Stats>(
|
|
139
|
+
* async ({ attributes, signal }) => {
|
|
140
|
+
* try {
|
|
141
|
+
* return await fetchThing(signal);
|
|
142
|
+
* } catch (err) {
|
|
143
|
+
* attributes.failures += 1;
|
|
144
|
+
* throw err;
|
|
145
|
+
* }
|
|
146
|
+
* },
|
|
147
|
+
* {
|
|
148
|
+
* intervalMs: 500,
|
|
149
|
+
* attributes: { failures: 0, startedAt: Date.now() },
|
|
150
|
+
* predicate: (_v, { attempt, attributes }) =>
|
|
151
|
+
* attempt < 20 && attributes.failures < 3,
|
|
152
|
+
* },
|
|
153
|
+
* )) {
|
|
154
|
+
* handle(x);
|
|
155
|
+
* }
|
|
156
|
+
*/
|
|
157
|
+
export declare function poll<T, A = Record<string, unknown>>(producer: PollProducer<T, A>, options: PollOptions<T, A>): AsyncGenerator<T, void, void>;
|
|
158
|
+
/**
|
|
159
|
+
* Tie a child `AbortController` to a parent signal. The child
|
|
160
|
+
* aborts whenever the parent aborts; aborting the child does not
|
|
161
|
+
* affect the parent (so a fetch-level cancel doesn't tear down the
|
|
162
|
+
* main poll loop).
|
|
163
|
+
*/
|
|
164
|
+
export declare function tieAbortSignal(child: AbortController, parent?: AbortSignal): void;
|
|
165
|
+
/**
|
|
166
|
+
* Mint a short, collision-resistant id by sampling the first `length`
|
|
167
|
+
* hex chars of a v4 UUID. `length` defaults to 8 (collision odds
|
|
168
|
+
* ~1 in 4 billion - safe within a single conversation turn / job /
|
|
169
|
+
* batch). Uses `globalThis.crypto.randomUUID()` so it works in
|
|
170
|
+
* both Node (>= 19) and modern browsers.
|
|
171
|
+
*
|
|
172
|
+
* Use for ids that the caller cares about being typeable / short
|
|
173
|
+
* (e.g. chart ids the LLM types into `[[chart:<id>]]` markers).
|
|
174
|
+
* For ids that need to survive across long-running batches or be
|
|
175
|
+
* globally unique, use a full UUID instead.
|
|
176
|
+
*/
|
|
177
|
+
export declare function shortId(length?: number): string;
|
|
178
|
+
export declare function fnvHash(...values: string[]): string;
|
|
179
|
+
export declare function fnvHashWithOptions(options?: {
|
|
180
|
+
length?: number;
|
|
181
|
+
alphabet?: string;
|
|
182
|
+
}, ...values: string[]): string;
|
|
183
|
+
export declare function toBase32(value: number, alphabet?: string, disableAlphabetValidation?: boolean): string;
|
|
184
|
+
export declare function isDatabricksAppEnv(env?: Record<string, string | undefined>): boolean;
|
|
185
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import fastDeepEqual from "fast-deep-equal";
|
|
2
|
+
export function memoize(fn, options) {
|
|
3
|
+
if (fn.length === 0) {
|
|
4
|
+
const factory = fn;
|
|
5
|
+
let cache;
|
|
6
|
+
return () => {
|
|
7
|
+
if (cache === undefined) {
|
|
8
|
+
cache = Promise.resolve().then(factory);
|
|
9
|
+
}
|
|
10
|
+
return cache;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const keyOf = options?.key ?? (defaultMemoizeKey);
|
|
14
|
+
const syncCache = new Map();
|
|
15
|
+
const asyncCache = new Map();
|
|
16
|
+
return (...args) => {
|
|
17
|
+
const key = keyOf(...args);
|
|
18
|
+
if (asyncCache.has(key)) {
|
|
19
|
+
return asyncCache.get(key);
|
|
20
|
+
}
|
|
21
|
+
if (syncCache.has(key)) {
|
|
22
|
+
return syncCache.get(key);
|
|
23
|
+
}
|
|
24
|
+
const result = fn(...args);
|
|
25
|
+
if (isThenable(result)) {
|
|
26
|
+
const pending = Promise.resolve(result);
|
|
27
|
+
asyncCache.set(key, pending);
|
|
28
|
+
void pending.catch(() => {
|
|
29
|
+
asyncCache.delete(key);
|
|
30
|
+
});
|
|
31
|
+
return pending;
|
|
32
|
+
}
|
|
33
|
+
syncCache.set(key, result);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Method decorator: memoizes the decorated method by its arguments.
|
|
39
|
+
*
|
|
40
|
+
* Requires `experimentalDecorators` in the consumer's `tsconfig.json`.
|
|
41
|
+
*/
|
|
42
|
+
export function memoized(_target, _propertyKey, descriptor) {
|
|
43
|
+
const original = descriptor.value;
|
|
44
|
+
if (typeof original !== "function") {
|
|
45
|
+
throw new TypeError("@memoized can only decorate methods");
|
|
46
|
+
}
|
|
47
|
+
descriptor.value = memoize(original);
|
|
48
|
+
return descriptor;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Async iterable that drives a periodic poll. Each iteration:
|
|
52
|
+
*
|
|
53
|
+
* 1. Builds a {@link PollContext} (`attempt`, `previous`, `signal`,
|
|
54
|
+
* shared `attributes`) and calls `producer(ctx)`; yields the
|
|
55
|
+
* resolved value.
|
|
56
|
+
* 2. Evaluates `options.predicate(value, ctx)`; stops when it
|
|
57
|
+
* returns (or resolves to) `false`.
|
|
58
|
+
* 3. Sleeps `options.intervalMs` before the next attempt.
|
|
59
|
+
*
|
|
60
|
+
* The first call runs immediately (no leading sleep) so the consumer
|
|
61
|
+
* sees a value without waiting an interval. Errors thrown by
|
|
62
|
+
* `producer` propagate through the generator.
|
|
63
|
+
*
|
|
64
|
+
* `poll` always creates an internal `AbortController` and exposes
|
|
65
|
+
* `internal.signal` as `ctx.signal`, so producers can rely on a
|
|
66
|
+
* defined signal without a nullish check. The external
|
|
67
|
+
* `options.signal` is tied in, and a `try/finally` aborts the
|
|
68
|
+
* internal signal when the consumer breaks out of the `for await`
|
|
69
|
+
* (or the loop throws), so any producer work still holding the
|
|
70
|
+
* signal sees the cancellation too.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* for await (const msg of poll(
|
|
74
|
+
* async ({ signal }) =>
|
|
75
|
+
* client.genie.getMessage({ ... }, { abortSignal: signal }),
|
|
76
|
+
* {
|
|
77
|
+
* intervalMs: 250,
|
|
78
|
+
* predicate: (m) => !TERMINAL_STATUSES.has(m.status),
|
|
79
|
+
* signal: controller.signal,
|
|
80
|
+
* },
|
|
81
|
+
* )) {
|
|
82
|
+
* render(msg);
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Typed attributes for per-run state.
|
|
87
|
+
* type Stats = { failures: number; startedAt: number };
|
|
88
|
+
* for await (const x of poll<Thing, Stats>(
|
|
89
|
+
* async ({ attributes, signal }) => {
|
|
90
|
+
* try {
|
|
91
|
+
* return await fetchThing(signal);
|
|
92
|
+
* } catch (err) {
|
|
93
|
+
* attributes.failures += 1;
|
|
94
|
+
* throw err;
|
|
95
|
+
* }
|
|
96
|
+
* },
|
|
97
|
+
* {
|
|
98
|
+
* intervalMs: 500,
|
|
99
|
+
* attributes: { failures: 0, startedAt: Date.now() },
|
|
100
|
+
* predicate: (_v, { attempt, attributes }) =>
|
|
101
|
+
* attempt < 20 && attributes.failures < 3,
|
|
102
|
+
* },
|
|
103
|
+
* )) {
|
|
104
|
+
* handle(x);
|
|
105
|
+
* }
|
|
106
|
+
*/
|
|
107
|
+
export async function* poll(producer, options) {
|
|
108
|
+
const { intervalMs, predicate, signal, attributes } = options;
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
if (signal)
|
|
111
|
+
tieAbortSignal(controller, signal);
|
|
112
|
+
// Single shared attributes object so writes from one iteration are
|
|
113
|
+
// visible on the next. `{} as A` is safe because either the caller
|
|
114
|
+
// supplied `attributes` (typed) or `A` defaulted to the unknown
|
|
115
|
+
// record shape (in which case `{}` satisfies it).
|
|
116
|
+
const sharedAttributes = attributes ?? {};
|
|
117
|
+
try {
|
|
118
|
+
let previous;
|
|
119
|
+
for (let attempt = 0;; attempt++) {
|
|
120
|
+
controller.signal.throwIfAborted();
|
|
121
|
+
const ctx = {
|
|
122
|
+
attempt,
|
|
123
|
+
previous,
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
attributes: sharedAttributes,
|
|
126
|
+
};
|
|
127
|
+
const value = await producer(ctx);
|
|
128
|
+
if (options.filter) {
|
|
129
|
+
if (options.filter === "distinct") {
|
|
130
|
+
if (fastDeepEqual(previous, value))
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
else if (!(await options.filter(value, ctx)))
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
yield value;
|
|
137
|
+
if (predicate && !(await predicate(value, ctx)))
|
|
138
|
+
return;
|
|
139
|
+
await sleep(intervalMs, controller.signal);
|
|
140
|
+
previous = value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
controller.abort();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function defaultMemoizeKey(...args) {
|
|
148
|
+
return JSON.stringify(args);
|
|
149
|
+
}
|
|
150
|
+
function isThenable(value) {
|
|
151
|
+
return (value !== null &&
|
|
152
|
+
typeof value === "object" &&
|
|
153
|
+
"then" in value &&
|
|
154
|
+
typeof value.then === "function");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Tie a child `AbortController` to a parent signal. The child
|
|
158
|
+
* aborts whenever the parent aborts; aborting the child does not
|
|
159
|
+
* affect the parent (so a fetch-level cancel doesn't tear down the
|
|
160
|
+
* main poll loop).
|
|
161
|
+
*/
|
|
162
|
+
export function tieAbortSignal(child, parent) {
|
|
163
|
+
if (!parent)
|
|
164
|
+
return;
|
|
165
|
+
else if (parent.aborted) {
|
|
166
|
+
child.abort(parent.reason);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
parent.addEventListener("abort", () => child.abort(parent.reason), {
|
|
170
|
+
once: true,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Promisified `setTimeout` that wakes up early (and rejects with
|
|
175
|
+
* `signal.reason`) when `signal` aborts mid-wait. Short-circuits to a
|
|
176
|
+
* rejected promise when the signal is already aborted on entry.
|
|
177
|
+
*/
|
|
178
|
+
function sleep(ms, signal) {
|
|
179
|
+
if (signal?.aborted)
|
|
180
|
+
return Promise.reject(signal.reason);
|
|
181
|
+
return new Promise((resolve, reject) => {
|
|
182
|
+
const onAbort = () => {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
reject(signal.reason);
|
|
185
|
+
};
|
|
186
|
+
const timer = setTimeout(() => {
|
|
187
|
+
signal?.removeEventListener("abort", onAbort);
|
|
188
|
+
resolve();
|
|
189
|
+
}, ms);
|
|
190
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Mint a short, collision-resistant id by sampling the first `length`
|
|
195
|
+
* hex chars of a v4 UUID. `length` defaults to 8 (collision odds
|
|
196
|
+
* ~1 in 4 billion - safe within a single conversation turn / job /
|
|
197
|
+
* batch). Uses `globalThis.crypto.randomUUID()` so it works in
|
|
198
|
+
* both Node (>= 19) and modern browsers.
|
|
199
|
+
*
|
|
200
|
+
* Use for ids that the caller cares about being typeable / short
|
|
201
|
+
* (e.g. chart ids the LLM types into `[[chart:<id>]]` markers).
|
|
202
|
+
* For ids that need to survive across long-running batches or be
|
|
203
|
+
* globally unique, use a full UUID instead.
|
|
204
|
+
*/
|
|
205
|
+
export function shortId(length = 8) {
|
|
206
|
+
return globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, length);
|
|
207
|
+
}
|
|
208
|
+
export function fnvHash(...values) {
|
|
209
|
+
return fnvHashWithOptions({}, ...values);
|
|
210
|
+
}
|
|
211
|
+
export function fnvHashWithOptions(options = {}, ...values) {
|
|
212
|
+
const { length = 6 } = options;
|
|
213
|
+
let digest = 0x811c9dc5;
|
|
214
|
+
for (const value of values) {
|
|
215
|
+
for (let i = 0; i < value.length; i++) {
|
|
216
|
+
digest ^= value.charCodeAt(i);
|
|
217
|
+
digest = Math.imul(digest, 0x01000193);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const alphabet = base32Alphabet(options.alphabet);
|
|
221
|
+
return toBase32(digest, alphabet, true)
|
|
222
|
+
.padStart(7, alphabet[0])
|
|
223
|
+
.slice(0, Math.min(length, 7));
|
|
224
|
+
}
|
|
225
|
+
const BASE32_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
226
|
+
function base32Alphabet(alphabet) {
|
|
227
|
+
if (alphabet === undefined)
|
|
228
|
+
return BASE32_ALPHABET;
|
|
229
|
+
else if (new Set(alphabet).size !== 32) {
|
|
230
|
+
throw new Error("Base32 alphabet must contain 32 unique characters");
|
|
231
|
+
}
|
|
232
|
+
return alphabet;
|
|
233
|
+
}
|
|
234
|
+
export function toBase32(value, alphabet, disableAlphabetValidation) {
|
|
235
|
+
if (!disableAlphabetValidation) {
|
|
236
|
+
alphabet = base32Alphabet(alphabet);
|
|
237
|
+
}
|
|
238
|
+
if (alphabet.length !== 32) {
|
|
239
|
+
throw new Error(`Base32 alphabet must contain exactly 32 characters, got ${alphabet.length}`);
|
|
240
|
+
}
|
|
241
|
+
value >>>= 0;
|
|
242
|
+
if (value === 0) {
|
|
243
|
+
return alphabet[0];
|
|
244
|
+
}
|
|
245
|
+
let result = "";
|
|
246
|
+
while (value > 0) {
|
|
247
|
+
result = alphabet[value & 31] + result;
|
|
248
|
+
value >>>= 5;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
export function isDatabricksAppEnv(env) {
|
|
253
|
+
env ??= typeof process !== "undefined" && process.env ? process.env : undefined;
|
|
254
|
+
if (!env) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const appName = env.DATABRICKS_APP_NAME?.trim();
|
|
258
|
+
const host = env.DATABRICKS_HOST?.trim();
|
|
259
|
+
const port = env.DATABRICKS_APP_PORT?.trim();
|
|
260
|
+
if (!appName || !host || !port) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const url = new URL(host);
|
|
265
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const portNumber = Number(port);
|
|
273
|
+
if (!Number.isInteger(portNumber) || portNumber < 1 || portNumber > 65535) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP header helpers shared across AppKit plugins: framework
|
|
3
|
+
* agnostic readers for HTTP headers and cookies that work uniformly
|
|
4
|
+
* across Express, Node `IncomingMessage`, WHATWG `Request` / `Response`
|
|
5
|
+
* / `Headers`, Hono, and any object that exposes a `headers` field of
|
|
6
|
+
* one of those shapes.
|
|
7
|
+
*
|
|
8
|
+
* Public API: {@link forEachHeaderValue}, {@link parseCookies}.
|
|
9
|
+
* Everything else (the header guards `isHeaders` / `isWrapped` /
|
|
10
|
+
* `unwrap`, the single cookie-header parser `parseCookieString`) is
|
|
11
|
+
* private to this module.
|
|
12
|
+
*
|
|
13
|
+
* URL parsing and path joining moved to `netUtils` (`./net.browser.ts`)
|
|
14
|
+
* so the URL surface lives next to the rest of the browser-safe
|
|
15
|
+
* networking helpers. The Databricks-aware REST helper that used to
|
|
16
|
+
* live here moved to `apiUtils.fetchApi` (`./api.ts`) so this module
|
|
17
|
+
* can stay dependency-free and browser-safe.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Anything that contains HTTP headers. Accepts:
|
|
21
|
+
*
|
|
22
|
+
* - A WHATWG `Headers` instance (fetch / undici / Hono `c.req.raw.headers`).
|
|
23
|
+
* - A header record (`Record<string, string | string[] | undefined>`),
|
|
24
|
+
* Node / Express style.
|
|
25
|
+
* - Any object with a `headers` field of one of the above. This covers
|
|
26
|
+
* Express `req`, Node `IncomingMessage`, WHATWG `Request` / `Response`,
|
|
27
|
+
* Hono `c.req.raw`, and similar shapes.
|
|
28
|
+
*/
|
|
29
|
+
export type HeaderLike = Headers | HeaderRecord | {
|
|
30
|
+
headers: Headers | HeaderRecord;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Single header value as exposed by Node `IncomingMessage.headers` and
|
|
34
|
+
* Express `req.headers` (string for most headers, array for repeated
|
|
35
|
+
* headers such as `Set-Cookie`).
|
|
36
|
+
*/
|
|
37
|
+
type HeaderValueLike = string[] | string | undefined;
|
|
38
|
+
/** Header bag with case-insensitive keys (Node / Express style). */
|
|
39
|
+
type HeaderRecord = Record<string, HeaderValueLike>;
|
|
40
|
+
/**
|
|
41
|
+
* Invokes `consumer` once per value for `headerName`, case-insensitive.
|
|
42
|
+
*
|
|
43
|
+
* - **Record input:** if the field is an array (e.g. repeated `Set-Cookie`),
|
|
44
|
+
* `consumer` runs once per array item.
|
|
45
|
+
* - **`Headers` input:** uses `get(name)` (which spec-joins repeats with
|
|
46
|
+
* `, `) except for `Set-Cookie`, which uses `getSetCookie()` so each
|
|
47
|
+
* cookie is delivered separately.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* forEachHeaderValue(req, "x-trace-id", (v) => spans.push(v)); // Express
|
|
51
|
+
* forEachHeaderValue(c.req.raw, "set-cookie", (v) => log(v)); // Hono
|
|
52
|
+
* forEachHeaderValue(headersInstance, "cookie", parse); // fetch
|
|
53
|
+
*/
|
|
54
|
+
export declare function forEachHeaderValue(input: HeaderLike | null | undefined, headerName: string, consumer: (value: string) => void): void;
|
|
55
|
+
/**
|
|
56
|
+
* Parses `Cookie` header values into a name-to-value map (URI-decoded).
|
|
57
|
+
*
|
|
58
|
+
* Accepts:
|
|
59
|
+
*
|
|
60
|
+
* - A raw `Cookie` string (`"a=1; b=2"`).
|
|
61
|
+
* - An array of such strings (e.g. multiple `Cookie` headers).
|
|
62
|
+
* - Any {@link HeaderLike}: a WHATWG `Headers` instance, a header
|
|
63
|
+
* record, or a request-like object with a `headers` field.
|
|
64
|
+
*
|
|
65
|
+
* First occurrence of each cookie name wins; later duplicates are ignored.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* parseCookies("session=abc; theme=dark");
|
|
69
|
+
* // { session: "abc", theme: "dark" }
|
|
70
|
+
*
|
|
71
|
+
* parseCookies(req); // Express / Node
|
|
72
|
+
* parseCookies(c.req.raw); // Hono
|
|
73
|
+
* parseCookies(request); // fetch Request
|
|
74
|
+
* parseCookies(request.headers); // WHATWG Headers directly
|
|
75
|
+
*/
|
|
76
|
+
export declare function parseCookies(input: HeaderLike | HeaderValueLike | null): Record<string, string>;
|
|
77
|
+
export {};
|