@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.
@@ -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 {};