@dbx-tools/shared-core 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ {
2
+ "dependencies": [
3
+ {
4
+ "name": "@types/node",
5
+ "version": "^24.6.0",
6
+ "type": "build"
7
+ },
8
+ {
9
+ "name": "consola",
10
+ "version": "catalog:",
11
+ "type": "build"
12
+ },
13
+ {
14
+ "name": "tsx",
15
+ "version": "^4.23.0",
16
+ "type": "build"
17
+ },
18
+ {
19
+ "name": "typescript",
20
+ "type": "build"
21
+ },
22
+ {
23
+ "name": "consola",
24
+ "version": "catalog:",
25
+ "type": "peer"
26
+ }
27
+ ],
28
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
29
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "files": [
3
+ ".projen/deps.json",
4
+ ".projen/files.json",
5
+ ".projen/tasks.json",
6
+ "package.json",
7
+ "test/tsconfig.json",
8
+ "tsconfig.json"
9
+ ],
10
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
11
+ }
@@ -0,0 +1,121 @@
1
+ {
2
+ "manifestVersion": 3,
3
+ "env": {
4
+ "PATH": "$(pnpm -c exec \"node --print process.env.PATH\")"
5
+ },
6
+ "tasks": {
7
+ "build": {
8
+ "name": "build",
9
+ "description": "Full release build",
10
+ "steps": [
11
+ {
12
+ "spawn": "pre-compile"
13
+ },
14
+ {
15
+ "spawn": "compile"
16
+ },
17
+ {
18
+ "spawn": "post-compile"
19
+ },
20
+ {
21
+ "spawn": "test"
22
+ },
23
+ {
24
+ "spawn": "package"
25
+ }
26
+ ]
27
+ },
28
+ "compile": {
29
+ "name": "compile",
30
+ "description": "Only compile",
31
+ "steps": [
32
+ {
33
+ "execArgs": [
34
+ "tsc",
35
+ "--build"
36
+ ]
37
+ }
38
+ ]
39
+ },
40
+ "default": {
41
+ "name": "default",
42
+ "description": "Synthesize project files",
43
+ "steps": [
44
+ {
45
+ "exec": "pnpm exec projen default",
46
+ "cwd": "../../.."
47
+ }
48
+ ]
49
+ },
50
+ "install": {
51
+ "name": "install",
52
+ "description": "Install project dependencies and update lockfile (non-frozen)",
53
+ "steps": [
54
+ {
55
+ "exec": "pnpm i --no-frozen-lockfile"
56
+ }
57
+ ]
58
+ },
59
+ "install:ci": {
60
+ "name": "install:ci",
61
+ "description": "Install project dependencies using frozen lockfile",
62
+ "steps": [
63
+ {
64
+ "exec": "pnpm i --frozen-lockfile"
65
+ }
66
+ ]
67
+ },
68
+ "package": {
69
+ "name": "package",
70
+ "description": "Creates the distribution package",
71
+ "steps": [
72
+ {
73
+ "execArgs": [
74
+ "mkdir",
75
+ "-p",
76
+ "dist/js"
77
+ ]
78
+ },
79
+ {
80
+ "execArgs": [
81
+ "pnpm",
82
+ "pack",
83
+ "--pack-destination",
84
+ "dist/js"
85
+ ]
86
+ }
87
+ ]
88
+ },
89
+ "post-compile": {
90
+ "name": "post-compile",
91
+ "description": "Runs after successful compilation"
92
+ },
93
+ "pre-compile": {
94
+ "name": "pre-compile",
95
+ "description": "Prepare the project for compilation"
96
+ },
97
+ "test": {
98
+ "name": "test",
99
+ "description": "Run tests",
100
+ "steps": [
101
+ {
102
+ "exec": "tsx --test 'test/**/*.test.ts'"
103
+ }
104
+ ]
105
+ },
106
+ "watch": {
107
+ "name": "watch",
108
+ "description": "Watch & compile in the background",
109
+ "steps": [
110
+ {
111
+ "execArgs": [
112
+ "tsc",
113
+ "--build",
114
+ "-w"
115
+ ]
116
+ }
117
+ ]
118
+ }
119
+ },
120
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\"."
121
+ }
package/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # @dbx-tools/shared-core
2
+
3
+ Browser-safe utility base for `@dbx-tools/*` packages.
4
+
5
+ Import this package for small, dependency-light helpers that can run in Node,
6
+ browsers, workers, CLIs, and shared schema packages. Modules are exported as
7
+ namespaces so call sites stay explicit:
8
+
9
+ ```ts
10
+ import {
11
+ async,
12
+ error,
13
+ hash,
14
+ http,
15
+ iterable,
16
+ log,
17
+ net,
18
+ object,
19
+ string,
20
+ } from "@dbx-tools/shared-core";
21
+ ```
22
+
23
+ Node-only helpers live in [`@dbx-tools/node-core`](../../node/core). AppKit and
24
+ Databricks SDK helpers live in [`@dbx-tools/node-appkit`](../../node/appkit).
25
+
26
+ Key features:
27
+
28
+ - Abort-aware async utilities for polling, sleeping, and connecting cancellation
29
+ across web and Node runtimes.
30
+ - Error normalization for unknown thrown values, nested causes, and HTTP-ish
31
+ status/message extraction.
32
+ - Deterministic non-cryptographic hashes and short ids for cache keys, slugs,
33
+ and generated names.
34
+ - String normalization helpers for slugs, identifiers, unique labels, and prompt
35
+ descriptions.
36
+ - Object, predicate, iterable, HTTP, cookie, network, token, memoization, and
37
+ logging helpers that avoid Node-only dependencies.
38
+ - Namespace exports that make utility call sites explicit without creating a
39
+ grab-bag default import.
40
+
41
+ ## Async Control
42
+
43
+ ```ts
44
+ for await (const status of async.poll(fetchStatus, {
45
+ intervalMs: 250,
46
+ timeoutMs: 30_000,
47
+ predicate: (s) => s !== "READY",
48
+ })) {
49
+ render(status);
50
+ }
51
+
52
+ await async.sleep(500, abortSignal);
53
+ ```
54
+
55
+ `async.poll()` is useful for Databricks APIs that expose long-running state.
56
+ `async.tieAbortSignal()` and `async.sleep()` let route handlers connect caller
57
+ cancellation to background work.
58
+
59
+ ## Error Handling
60
+
61
+ ```ts
62
+ try {
63
+ await run();
64
+ } catch (err) {
65
+ logger.warn("run failed", { error: error.errorMessage(err) });
66
+ const ctx = error.errorContext(err);
67
+ return Response.json({ message: ctx.message }, { status: ctx.status ?? 500 });
68
+ }
69
+ ```
70
+
71
+ `error.toError()`, `error.errorMessage()`, `error.errorMessages()`, and
72
+ `error.errorNodes()` normalize unknown thrown values. `error.errorContext()`
73
+ extracts HTTP-ish status/message detail from nested errors.
74
+
75
+ ## Hashes And Ids
76
+
77
+ ```ts
78
+ const id = hash.id(8);
79
+ const cacheKey = hash.fnvHash("workspace", host, endpointName);
80
+ const suffix = hash.fnvHashWithOptions({ length: 6 }, longName);
81
+ ```
82
+
83
+ These hashes are deterministic and non-cryptographic. Use them for cache keys,
84
+ slug suffixes, and trace-stable identifiers, not secrets or signatures.
85
+
86
+ ## Strings And Descriptions
87
+
88
+ ```ts
89
+ const slug = string.toSlug("My Cool Project!");
90
+ const id = string.toIdentifierWithOptions({ delimiter: "_" }, "Model Name");
91
+ const unique = string.toUniqueSlug("Send Email", { fallbackPrefix: "tool" });
92
+ const description = string.toDescription([
93
+ "Answer with SQL first.",
94
+ { "When data is missing": "Say what is missing." },
95
+ ]);
96
+ ```
97
+
98
+ `string.tokenize()`, `toSlug()`, and `toIdentifier()` keep package names, tool
99
+ ids, schema ids, and generated labels consistent. `toDescription()` turns nested
100
+ description data into prompt/tool text without hand-concatenating paragraphs.
101
+
102
+ ## Objects And Predicates
103
+
104
+ ```ts
105
+ if (object.isRecord(value)) {
106
+ const enabled = object.toBoolean(value.enabled);
107
+ }
108
+
109
+ const same = object.deepEqual(left, right);
110
+
111
+ const isRunnable = predicate
112
+ .create((pkg: Package) => pkg.tags.includes("node"))
113
+ .and((pkg) => pkg.name.includes("appkit"));
114
+ ```
115
+
116
+ `object.deepEqual()` supports an optional comparator for domain-specific
117
+ short-circuits. `predicate.create()` returns composable predicates with `and`,
118
+ `or`, and `negate`, used heavily by the projen engine.
119
+
120
+ ## Iterables
121
+
122
+ ```ts
123
+ const names = iterable
124
+ .sequence(packages)
125
+ .map((p) => p.name)
126
+ .filter(Boolean)
127
+ .distinct()
128
+ .toArray();
129
+
130
+ const grouped = iterable.group(packages, {
131
+ node: (p) => p.tags.includes("node"),
132
+ ui: (p) => p.tags.includes("ui"),
133
+ });
134
+ ```
135
+
136
+ The iterable helpers are lazy and work well for filesystem scans, package lists,
137
+ and one-pass generated data. Use `sequence(..., { cache: true })` when a source
138
+ must be re-read.
139
+
140
+ ## HTTP Headers And Cookies
141
+
142
+ ```ts
143
+ const cookies = http.parseCookies(req);
144
+
145
+ let bearer: string | undefined;
146
+ http.forEachHeaderValue(req, "authorization", (value) => {
147
+ if (value.startsWith("Bearer ")) bearer = value.slice("Bearer ".length);
148
+ });
149
+ ```
150
+
151
+ `http.HeaderLike` works with Fetch `Request`, Express-ish requests, Node header
152
+ records, and plain `{ headers }` objects. `http.createFetchError()` turns a
153
+ failed `Response` into an error with response text attached.
154
+
155
+ ## Network Strings, Email, And CIDR
156
+
157
+ ```ts
158
+ const url = net.urlBuilder("example.com")?.withPathAppend("api", "2.0");
159
+ const emails = net.parseEmails("alice@example.com; bob@example.com");
160
+ const cidr = net.parseCidr("10.0.0.0/8");
161
+ const internal = cidr ? net.ipInCidr("10.1.2.3", cidr) : false;
162
+ ```
163
+
164
+ `net.urlBuilder()` is a forgiving URL builder for config and REST helpers.
165
+ `net.pathMatch()` compares path prefixes on segment boundaries. IP/CIDR helpers
166
+ parse IPv4 and IPv6 into a shared bigint comparison model.
167
+
168
+ ## Token Claims
169
+
170
+ ```ts
171
+ const scopes = token.getAccessTokenScopes(req, "x-forwarded-access-token");
172
+ const canReadWorkspace = token.includesAccessTokenScope(scopes, ["workspace", "all-apis"]);
173
+ ```
174
+
175
+ Token helpers decode JWT payloads without validating signatures. Use them for
176
+ request-scoped authorization hints after the platform has already authenticated
177
+ the request.
178
+
179
+ ## Memoization
180
+
181
+ ```ts
182
+ const getRanges = functionModule.memoize(fetchRanges, {
183
+ ttlMs: 24 * 60 * 60 * 1000,
184
+ });
185
+ ```
186
+
187
+ `functionModule.memoize()` caches sync or async factories, evicts rejected
188
+ promises, and supports TTL-based refresh. It is useful for public metadata feeds,
189
+ SDK catalogues, and expensive computed constants.
190
+
191
+ ## Logging
192
+
193
+ ```ts
194
+ const logger = log.logger("mastra/genie");
195
+ logger.info("space:resolved", { spaceId });
196
+
197
+ if (log.isLevelEnabled("debug")) {
198
+ logger.debug("large payload", expensivePayload());
199
+ }
200
+ ```
201
+
202
+ `log.logger()` uses `consola` when installed and falls back to `console`. It
203
+ honors `LOG_LEVEL` per call, so debug statements can stay in production code
204
+ without paying formatting cost when disabled.
205
+
206
+ ## Modules
207
+
208
+ - `async` - polling, sleep, and abort-signal wiring.
209
+ - `error` - unknown-error normalization and HTTP-ish error context.
210
+ - `hash` - ids, FNV hashes, and base32 encoding.
211
+ - `string` - tokenization, slugs, identifiers, descriptions, pluralization, and
212
+ HTML escaping.
213
+ - `object` - record checks, boolean coercion, deep equality, and shape types.
214
+ - `iterable` - lazy sequence transforms and collection helpers.
215
+ - `predicate` - composable boolean/type predicates.
216
+ - `http` - header iteration, cookie parsing, and fetch error creation.
217
+ - `net` - URL building, email parsing, path matching, IP/CIDR helpers.
218
+ - `token` - JWT payload and scope readers.
219
+ - `functionModule` - memoization.
220
+ - `log` - tagged leveled logging.
package/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ // GENERATED by projen watch - DO NOT EDIT.
2
+ // Regenerated from the exporting modules in ./src.
3
+ // Hand edits are overwritten on the next watch; this file is read-only.
4
+
5
+ export * as async from "./src/async";
6
+ export * as error from "./src/error";
7
+ export * as functionModule from "./src/function";
8
+ export * as hash from "./src/hash";
9
+ export * as http from "./src/http";
10
+ export * as iterable from "./src/iterable";
11
+ export * as log from "./src/log";
12
+ export * as net from "./src/net";
13
+ export * as object from "./src/object";
14
+ export * as predicate from "./src/predicate";
15
+ export * as string from "./src/string";
16
+ export * as token from "./src/token";
17
+ export type { PollContext, PollProducer, PollOptions } from "./src/async";
18
+ export type { ErrorContext } from "./src/error";
19
+ export type { MemoizeOptions } from "./src/function";
20
+ export type { HeaderLike } from "./src/http";
21
+ export type { Sequence, Container, Collection, OneOrMany } from "./src/iterable";
22
+ export type { LogLevel, Logger } from "./src/log";
23
+ export type { UrlLike, IpVersion, ParsedIp, Cidr, UrlBuilder, ParseEmailsOptions } from "./src/net";
24
+ export type { NameLike, NonFunctionKeys, DeepEqualComparator } from "./src/object";
25
+ export type { PredicateFunction, TypePredicateFunction, PredicateInput, Predicate } from "./src/predicate";
26
+ export type { TokenizeOptions, KeyOptions, IdentifierOptions, Description } from "./src/string";
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@dbx-tools/shared-core",
3
+ "devDependencies": {
4
+ "@types/node": "^24.6.0",
5
+ "consola": "^3.4.2",
6
+ "tsx": "^4.23.0",
7
+ "typescript": "^5.9.3"
8
+ },
9
+ "peerDependencies": {
10
+ "consola": "^3.4.2"
11
+ },
12
+ "main": "index.ts",
13
+ "license": "UNLICENSED",
14
+ "version": "0.1.2",
15
+ "types": "index.ts",
16
+ "type": "module",
17
+ "exports": {
18
+ ".": "./index.ts",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "peerDependenciesMeta": {
22
+ "consola": {
23
+ "optional": true
24
+ }
25
+ },
26
+ "dbxToolsConfig": {
27
+ "tags": [
28
+ "shared"
29
+ ]
30
+ },
31
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
32
+ "scripts": {
33
+ "build": "projen build",
34
+ "compile": "projen compile",
35
+ "default": "projen default",
36
+ "package": "projen package",
37
+ "post-compile": "projen post-compile",
38
+ "pre-compile": "projen pre-compile",
39
+ "test": "projen test",
40
+ "watch": "projen watch",
41
+ "projen": "projen"
42
+ }
43
+ }
package/src/async.ts ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Cancellation-aware async primitives: an abortable {@link sleep}, an
3
+ * {@link AbortController} linker ({@link tieAbortSignal}), and a periodic
4
+ * {@link poll} generator. Dependency-free; `poll`'s `"distinct"` filter
5
+ * uses the local {@link deepEqual}.
6
+ */
7
+ import { deepEqual } from "./object";
8
+
9
+ /**
10
+ * Per-iteration context handed to {@link PollProducer} and the
11
+ * predicate on each step of a {@link poll} loop. Bundles the
12
+ * iteration metadata so the call signatures stay stable as `poll`
13
+ * grows additional fields.
14
+ *
15
+ * `signal` is owned by `poll`: it tracks the external
16
+ * `PollOptions.signal` (when supplied) and also fires when the
17
+ * consumer breaks out of the loop, so producers can forward it to
18
+ * any in-flight work (`fetch`, SDK calls, etc.) and have a single
19
+ * cancellation source tear down both the request and the loop.
20
+ *
21
+ * `attributes` is a mutable scratchpad shared across every
22
+ * iteration of a single `poll` run. The same object reference is
23
+ * passed each call so writes from one iteration are visible to the
24
+ * next - useful for stashing per-loop state (retry counters, start
25
+ * timestamps, anything you'd otherwise close over via a let).
26
+ * Generic `A` lets callers type the bag; defaults to
27
+ * `Record<string, unknown>`.
28
+ */
29
+ export interface PollContext<T, A = Record<string, unknown>> {
30
+ /** Zero-based iteration index (`0` on the first call). */
31
+ attempt: number;
32
+ /** Value yielded on the prior iteration; `undefined` on the first. */
33
+ previous: T | undefined;
34
+ /** Cancellation handle. Always defined; forward to in-flight work. */
35
+ signal: AbortSignal;
36
+ /** Per-run mutable scratchpad shared across iterations. */
37
+ attributes: A;
38
+ }
39
+
40
+ /** One step of a {@link poll} loop. See {@link PollContext}. */
41
+ export type PollProducer<T, A = Record<string, unknown>> = (
42
+ ctx: PollContext<T, A>,
43
+ ) => T | PromiseLike<T>;
44
+
45
+ export interface PollOptions<T, A = Record<string, unknown>> {
46
+ /** Milliseconds to wait between polls. */
47
+ intervalMs: number;
48
+ /**
49
+ * Predicate evaluated against each yielded value: return `true` to
50
+ * keep it, `false` to skip it (without stopping the loop). May be
51
+ * sync or async - a `PromiseLike<boolean>` is awaited before the
52
+ * decision is made. Receives the same {@link PollContext} as the
53
+ * producer (same `signal`, same `attributes` bag). The special
54
+ * value `"distinct"` skips a value that deep-equals the previous
55
+ * one.
56
+ */
57
+ filter?: ((value: T, ctx: PollContext<T, A>) => boolean | PromiseLike<boolean>) | "distinct";
58
+ /**
59
+ * Predicate evaluated against each yielded value: return `true` to
60
+ * keep polling, `false` to stop. May be sync or async. Omit to poll
61
+ * forever (the consumer stops by breaking out of the loop or by
62
+ * aborting `signal`).
63
+ */
64
+ predicate?: (value: T, ctx: PollContext<T, A>) => boolean | PromiseLike<boolean>;
65
+ /**
66
+ * External cancellation handle. Tied into the internal signal that
67
+ * `poll` hands to `producer`, so aborting it tears down both the
68
+ * in-flight request and the inter-poll sleep.
69
+ */
70
+ signal?: AbortSignal;
71
+ /**
72
+ * Hard upper bound on the total lifetime of the poll loop, in
73
+ * milliseconds. When the budget elapses, `poll` aborts its internal
74
+ * signal so the in-flight producer and inter-poll sleep both tear
75
+ * down promptly, and the loop throws the `TimeoutError`
76
+ * `DOMException` produced by `AbortSignal.timeout(timeoutMs)`. The
77
+ * budget starts ticking the moment the generator is created.
78
+ */
79
+ timeoutMs?: number;
80
+ /**
81
+ * Initial value for `ctx.attributes`. Defaults to `{}`. The same
82
+ * object is reused across iterations, so callers can pre-populate
83
+ * fields (timers, retry counters, etc.) and the producer /
84
+ * predicate can mutate them in place.
85
+ */
86
+ attributes?: A;
87
+ }
88
+
89
+ /**
90
+ * Async iterable that drives a periodic poll. Each iteration:
91
+ *
92
+ * 1. Builds a {@link PollContext} (`attempt`, `previous`, `signal`,
93
+ * shared `attributes`) and calls `producer(ctx)`; yields the
94
+ * resolved value (subject to `filter`).
95
+ * 2. Evaluates `options.predicate(value, ctx)`; stops when it
96
+ * returns (or resolves to) `false`.
97
+ * 3. Sleeps `options.intervalMs` before the next attempt.
98
+ *
99
+ * The first call runs immediately (no leading sleep) so the consumer
100
+ * sees a value without waiting an interval. Errors thrown by
101
+ * `producer` propagate through the generator.
102
+ *
103
+ * `poll` always creates an internal `AbortController` and exposes
104
+ * `internal.signal` as `ctx.signal`, so producers can rely on a
105
+ * defined signal without a nullish check. The external
106
+ * `options.signal` is tied in, and a `try/finally` aborts the
107
+ * internal signal when the consumer breaks out of the `for await`
108
+ * (or the loop throws), so any producer work still holding the
109
+ * signal sees the cancellation too.
110
+ *
111
+ * @example
112
+ * for await (const msg of poll(
113
+ * async ({ signal }) =>
114
+ * client.genie.getMessage({ ... }, { abortSignal: signal }),
115
+ * {
116
+ * intervalMs: 250,
117
+ * predicate: (m) => !TERMINAL_STATUSES.has(m.status),
118
+ * signal: controller.signal,
119
+ * },
120
+ * )) {
121
+ * render(msg);
122
+ * }
123
+ */
124
+ export async function* poll<T, A = Record<string, unknown>>(
125
+ producer: PollProducer<T, A>,
126
+ options: PollOptions<T, A>,
127
+ ): AsyncGenerator<T, void, void> {
128
+ const { intervalMs, predicate, signal, attributes, timeoutMs } = options;
129
+ const controller = new AbortController();
130
+ if (signal) tieAbortSignal(controller, signal);
131
+ if (timeoutMs !== undefined) {
132
+ tieAbortSignal(controller, AbortSignal.timeout(timeoutMs));
133
+ }
134
+ // Single shared attributes object so writes from one iteration are
135
+ // visible on the next. `{} as A` is safe because either the caller
136
+ // supplied `attributes` (typed) or `A` defaulted to the unknown
137
+ // record shape (in which case `{}` satisfies it).
138
+ const sharedAttributes = attributes ?? ({} as A);
139
+ try {
140
+ let previous: T | undefined;
141
+ for (let attempt = 0; ; attempt++) {
142
+ controller.signal.throwIfAborted();
143
+ const ctx: PollContext<T, A> = {
144
+ attempt,
145
+ previous,
146
+ signal: controller.signal,
147
+ attributes: sharedAttributes,
148
+ };
149
+ const value = await producer(ctx);
150
+ if (options.filter) {
151
+ if (options.filter === "distinct") {
152
+ if (deepEqual(previous, value)) continue;
153
+ } else if (!(await options.filter(value, ctx))) continue;
154
+ }
155
+ yield value;
156
+ if (predicate && !(await predicate(value, ctx))) return;
157
+ await sleep(intervalMs, controller.signal);
158
+ previous = value;
159
+ }
160
+ } finally {
161
+ controller.abort();
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Tie a child `AbortController` to a parent signal. The child aborts
167
+ * whenever the parent aborts; aborting the child does not affect the
168
+ * parent (so a fetch-level cancel doesn't tear down the main poll loop).
169
+ */
170
+ export function tieAbortSignal(child: AbortController, parent?: AbortSignal): void {
171
+ if (!parent) return;
172
+ else if (parent.aborted) {
173
+ child.abort(parent.reason);
174
+ return;
175
+ }
176
+ parent.addEventListener("abort", () => child.abort(parent.reason), {
177
+ once: true,
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Promisified `setTimeout` that wakes up early (and rejects with
183
+ * `signal.reason`) when `signal` aborts mid-wait. Short-circuits to a
184
+ * rejected promise when the signal is already aborted on entry, so the
185
+ * abort path is consistent regardless of whether the wait actually
186
+ * started.
187
+ *
188
+ * Use as the building block for any "wait, but cancel cleanly" pattern -
189
+ * inter-poll backoff, pacing loops, retry timers, long-poll budgets - so
190
+ * cancellation always rejects with the caller's `signal.reason` rather
191
+ * than silently resolving after the timer expires.
192
+ *
193
+ * @example
194
+ * await sleep(250, req.signal);
195
+ */
196
+ export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
197
+ if (signal?.aborted) return Promise.reject(signal.reason);
198
+ return new Promise((resolve, reject) => {
199
+ const onAbort = (): void => {
200
+ clearTimeout(timer);
201
+ reject(signal!.reason);
202
+ };
203
+ const timer = setTimeout(() => {
204
+ signal?.removeEventListener("abort", onAbort);
205
+ resolve();
206
+ }, ms);
207
+ signal?.addEventListener("abort", onAbort, { once: true });
208
+ });
209
+ }