@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.
- package/.projen/deps.json +29 -0
- package/.projen/files.json +11 -0
- package/.projen/tasks.json +121 -0
- package/README.md +220 -0
- package/index.ts +26 -0
- package/package.json +43 -0
- package/src/async.ts +209 -0
- package/src/error.ts +178 -0
- package/src/function.ts +91 -0
- package/src/hash.ts +261 -0
- package/src/http.ts +223 -0
- package/src/iterable.ts +790 -0
- package/src/log.ts +380 -0
- package/src/net.ts +535 -0
- package/src/object.ts +165 -0
- package/src/predicate.ts +151 -0
- package/src/string.ts +483 -0
- package/src/token.ts +136 -0
- package/test/tsconfig.json +14 -0
- package/tsconfig.json +40 -0
|
@@ -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,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
|
+
}
|