hanzio 1.1.0 → 2.0.0
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 +832 -219
- package/dist/api-wrapper/builders.d.ts +15 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +40 -2
- package/dist/api-wrapper/headers.d.ts +2 -1
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +12 -10
- package/dist/api-wrapper/types.d.ts +104 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +45 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-181e6vfw.js +29 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-srt06xd2.js +15 -0
- package/dist/index-wjyb9bsd.js +263 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +13 -25
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +8 -13
- package/dist/secrets/loaders.d.ts +5 -12
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1100
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +863 -2809
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +261 -1377
- package/dist/src/secrets/vite.js +6 -17
- package/dist/src/secrets/worker.js +12 -0
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +347 -174
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +37 -26
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +87 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +84 -52
- package/src/api-wrapper/create-client.ts +246 -96
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +117 -2
- package/src/api-wrapper/headers.ts +19 -1
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +43 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +68 -74
- package/src/api-wrapper/types.ts +186 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +235 -83
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +79 -210
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +214 -66
- package/src/secrets/loaders.ts +24 -48
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +134 -103
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/infisical.js +0 -1194
- package/dist/zod/index.js +0 -210
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** `Object.keys` typed as the object's string keys. */
|
|
2
|
+
export function typedKeys<T extends object>(object: T): (keyof T & string)[] {
|
|
3
|
+
return Object.keys(object) as (keyof T & string)[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** `Object.entries` typed as `[key, value]` pairs per key. */
|
|
7
|
+
export function typedEntries<T extends object>(
|
|
8
|
+
object: T
|
|
9
|
+
): { [K in keyof T & string]: [K, T[K]] }[keyof T & string][] {
|
|
10
|
+
return Object.entries(object) as {
|
|
11
|
+
[K in keyof T & string]: [K, T[K]]
|
|
12
|
+
}[keyof T & string][]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `Object.fromEntries` typed as `Record<K, V>`. */
|
|
16
|
+
export function typedFromEntries<const K extends PropertyKey, V>(
|
|
17
|
+
entries: Iterable<readonly [K, V]>
|
|
18
|
+
): Record<K, V> {
|
|
19
|
+
return Object.fromEntries(entries) as Record<K, V>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Builds a record with one entry per key. */
|
|
23
|
+
export function recordFromKeys<const K extends PropertyKey, V>(
|
|
24
|
+
keys: readonly K[],
|
|
25
|
+
fn: (key: K) => V
|
|
26
|
+
): Record<K, V> {
|
|
27
|
+
return typedFromEntries(keys.map((key) => [key, fn(key)] as const))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function mapValues<T extends object, R>(
|
|
31
|
+
object: T,
|
|
32
|
+
fn: (value: T[keyof T & string], key: keyof T & string) => R
|
|
33
|
+
): Record<keyof T & string, R> {
|
|
34
|
+
return typedFromEntries(
|
|
35
|
+
typedEntries(object).map(([key, value]) => [key, fn(value, key)] as const)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function pick<T extends object, const K extends keyof T>(
|
|
40
|
+
object: T,
|
|
41
|
+
keys: readonly K[]
|
|
42
|
+
): Pick<T, K> {
|
|
43
|
+
const result = {} as Pick<T, K>
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
if (key in object) result[key] = object[key]
|
|
46
|
+
}
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function omit<T extends object, const K extends keyof T>(
|
|
51
|
+
object: T,
|
|
52
|
+
keys: readonly K[]
|
|
53
|
+
): Omit<T, K> {
|
|
54
|
+
const result = { ...object }
|
|
55
|
+
for (const key of keys) delete result[key]
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Path = string | readonly (string | number)[]
|
|
60
|
+
|
|
61
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
62
|
+
|
|
63
|
+
function toPath(path: Path): readonly (string | number)[] {
|
|
64
|
+
if (typeof path !== 'string') return path
|
|
65
|
+
return path === '' ? [] : path.split('.')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Reads a nested value by dot path (`a.b.0.c`) or segment array; undefined when missing. */
|
|
69
|
+
export function getAtPath(object: unknown, path: Path): unknown {
|
|
70
|
+
let current = object
|
|
71
|
+
for (const key of toPath(path)) {
|
|
72
|
+
if (current === null || typeof current !== 'object') return undefined
|
|
73
|
+
current = (current as Record<PropertyKey, unknown>)[key]
|
|
74
|
+
}
|
|
75
|
+
return current
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Writes a nested value in place, creating arrays for numeric segments and objects otherwise. */
|
|
79
|
+
export function setAtPath(object: object, path: Path, value: unknown): void {
|
|
80
|
+
const keys = toPath(path)
|
|
81
|
+
if (keys.length === 0) throw new Error('Path must not be empty')
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
if (UNSAFE_KEYS.has(String(key))) {
|
|
84
|
+
throw new Error(`Unsafe path segment: ${key}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let current = object as Record<PropertyKey, unknown>
|
|
89
|
+
for (let index = 0; index < keys.length - 1; index++) {
|
|
90
|
+
const key = keys[index]!
|
|
91
|
+
let next = current[key]
|
|
92
|
+
if (next === null || typeof next !== 'object') {
|
|
93
|
+
next = /^\d+$/.test(String(keys[index + 1])) ? [] : {}
|
|
94
|
+
current[key] = next
|
|
95
|
+
}
|
|
96
|
+
current = next as Record<PropertyKey, unknown>
|
|
97
|
+
}
|
|
98
|
+
current[keys.at(-1)!] = value
|
|
99
|
+
}
|
package/src/p-queue/index.ts
CHANGED
package/src/promise/index.ts
CHANGED
|
@@ -5,27 +5,37 @@ export type BackgroundPromiseOptions = {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export type Success<T> = {
|
|
8
|
+
ok: true
|
|
8
9
|
data: T
|
|
9
10
|
error: null
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export type Failure<E> = {
|
|
14
|
+
ok: false
|
|
13
15
|
data: null
|
|
14
16
|
error: E
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export type Result<T, E = Error> = Success<T> | Failure<E>
|
|
18
20
|
|
|
21
|
+
export function ok<T>(data: T): Success<T> {
|
|
22
|
+
return { ok: true, data, error: null }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function err<E>(error: E): Failure<E> {
|
|
26
|
+
return { ok: false, data: null, error }
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function isSuccess<T, E>(result: Result<T, E>): result is Success<T> {
|
|
20
|
-
return result.
|
|
30
|
+
return result.ok
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export function isFailure<T, E>(result: Result<T, E>): result is Failure<E> {
|
|
24
|
-
return result.
|
|
34
|
+
return !result.ok
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export function unwrapResult<T, E>(result: Result<T, E>): T {
|
|
28
|
-
if (
|
|
38
|
+
if (result.ok) return result.data
|
|
29
39
|
throw result.error
|
|
30
40
|
}
|
|
31
41
|
|
|
@@ -57,17 +67,302 @@ export function backgroundPromiseSync<T>(
|
|
|
57
67
|
})
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
/** Resolves after `ms`; rejects with `signal.reason` if the signal aborts first. */
|
|
71
|
+
export function promiseTimeout(
|
|
72
|
+
ms: number,
|
|
73
|
+
options: { signal?: AbortSignal } = {}
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const { signal } = options
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
if (signal?.aborted) {
|
|
78
|
+
reject(signal.reason)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const onAbort = () => {
|
|
82
|
+
clearTimeout(timer)
|
|
83
|
+
reject(signal?.reason)
|
|
84
|
+
}
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
signal?.removeEventListener('abort', onAbort)
|
|
87
|
+
resolve()
|
|
88
|
+
}, ms)
|
|
89
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
90
|
+
})
|
|
62
91
|
}
|
|
63
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Accepts a promise or a function; sync throws inside the function are
|
|
95
|
+
* captured too.
|
|
96
|
+
*/
|
|
64
97
|
export async function tryCatch<T, E = Error>(
|
|
65
|
-
|
|
98
|
+
input: Promise<T> | (() => T | Promise<T>)
|
|
66
99
|
): Promise<Result<T, E>> {
|
|
67
100
|
try {
|
|
68
|
-
|
|
69
|
-
return { data, error: null }
|
|
101
|
+
return ok(await (typeof input === 'function' ? input() : input))
|
|
70
102
|
} catch (error) {
|
|
71
|
-
return
|
|
103
|
+
return err(error as E)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class PromiseTimeoutError extends Error {
|
|
108
|
+
readonly timeoutMs: number
|
|
109
|
+
|
|
110
|
+
constructor(timeoutMs: number, message?: string) {
|
|
111
|
+
super(message ?? `Promise timed out after ${timeoutMs}ms`)
|
|
112
|
+
this.name = 'PromiseTimeoutError'
|
|
113
|
+
this.timeoutMs = timeoutMs
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Rejects with `PromiseTimeoutError` after `ms`, or with `signal.reason` when
|
|
119
|
+
* the signal aborts. Pass a function to receive a signal that aborts on
|
|
120
|
+
* timeout, so the underlying work can stop too.
|
|
121
|
+
*/
|
|
122
|
+
export async function withTimeout<T>(
|
|
123
|
+
input: Promise<T> | ((signal: AbortSignal) => Promise<T>),
|
|
124
|
+
ms: number,
|
|
125
|
+
options: { signal?: AbortSignal; message?: string } = {}
|
|
126
|
+
): Promise<T> {
|
|
127
|
+
const controller = new AbortController()
|
|
128
|
+
const onAbort = () => controller.abort(options.signal?.reason)
|
|
129
|
+
if (options.signal?.aborted) onAbort()
|
|
130
|
+
else options.signal?.addEventListener('abort', onAbort, { once: true })
|
|
131
|
+
|
|
132
|
+
const timer = setTimeout(
|
|
133
|
+
() => controller.abort(new PromiseTimeoutError(ms, options.message)),
|
|
134
|
+
ms
|
|
135
|
+
)
|
|
136
|
+
let rejectOnAbort: () => void = () => {}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
140
|
+
rejectOnAbort = () => reject(controller.signal.reason)
|
|
141
|
+
controller.signal.addEventListener('abort', rejectOnAbort, { once: true })
|
|
142
|
+
})
|
|
143
|
+
if (controller.signal.aborted) throw controller.signal.reason
|
|
144
|
+
|
|
145
|
+
const promise =
|
|
146
|
+
typeof input === 'function' ? input(controller.signal) : input
|
|
147
|
+
return await Promise.race([promise, aborted])
|
|
148
|
+
} finally {
|
|
149
|
+
clearTimeout(timer)
|
|
150
|
+
options.signal?.removeEventListener('abort', onAbort)
|
|
151
|
+
controller.signal.removeEventListener('abort', rejectOnAbort)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type BackoffOptions = {
|
|
156
|
+
/** Delay for the first retry (default 300ms). */
|
|
157
|
+
baseMs?: number
|
|
158
|
+
/** Upper bound for any delay (default 30s). */
|
|
159
|
+
maxMs?: number
|
|
160
|
+
/**
|
|
161
|
+
* `true`/`'full'`: random delay in [0, computed] (default).
|
|
162
|
+
* `'partial'`: computed plus 0–`jitterRatio` extra (may exceed `maxMs`).
|
|
163
|
+
*/
|
|
164
|
+
jitter?: boolean | 'full' | 'partial'
|
|
165
|
+
/** Max extra fraction for `'partial'` jitter (default 0.25). */
|
|
166
|
+
jitterRatio?: number
|
|
167
|
+
/** Source of randomness in [0, 1) (default `Math.random`). */
|
|
168
|
+
random?: () => number
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Exponential backoff delay for a 1-based retry attempt. */
|
|
172
|
+
export function exponentialBackoff(
|
|
173
|
+
attempt: number,
|
|
174
|
+
options: BackoffOptions = {}
|
|
175
|
+
): number {
|
|
176
|
+
const {
|
|
177
|
+
baseMs = 300,
|
|
178
|
+
maxMs = 30_000,
|
|
179
|
+
jitter = true,
|
|
180
|
+
jitterRatio = 0.25,
|
|
181
|
+
random = Math.random
|
|
182
|
+
} = options
|
|
183
|
+
const delay = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1))
|
|
184
|
+
if (jitter === 'partial')
|
|
185
|
+
return Math.round(delay * (1 + jitterRatio * random()))
|
|
186
|
+
return jitter ? Math.round(random() * delay) : delay
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type RetryOptions = {
|
|
190
|
+
/** Retries after the first attempt (default 3). */
|
|
191
|
+
retries?: number
|
|
192
|
+
/** Delay before retry `attempt` (1-based). Default: `exponentialBackoff`. */
|
|
193
|
+
delayMs?: number | ((attempt: number, error: unknown) => number)
|
|
194
|
+
/** Return false to stop retrying and rethrow (default: always retry). */
|
|
195
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean
|
|
196
|
+
onRetry?: (error: unknown, attempt: number, delayMs: number) => void
|
|
197
|
+
signal?: AbortSignal
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function retry<T>(
|
|
201
|
+
fn: (context: { attempt: number; signal?: AbortSignal }) => Promise<T> | T,
|
|
202
|
+
options: RetryOptions = {}
|
|
203
|
+
): Promise<T> {
|
|
204
|
+
const { retries = 3, delayMs, shouldRetry, onRetry, signal } = options
|
|
205
|
+
|
|
206
|
+
for (let attempt = 0; ; attempt++) {
|
|
207
|
+
signal?.throwIfAborted()
|
|
208
|
+
try {
|
|
209
|
+
return await fn({ attempt, signal })
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const nextAttempt = attempt + 1
|
|
212
|
+
if (attempt >= retries || shouldRetry?.(error, nextAttempt) === false) {
|
|
213
|
+
throw error
|
|
214
|
+
}
|
|
215
|
+
const delay =
|
|
216
|
+
delayMs === undefined
|
|
217
|
+
? exponentialBackoff(nextAttempt)
|
|
218
|
+
: typeof delayMs === 'function'
|
|
219
|
+
? delayMs(nextAttempt, error)
|
|
220
|
+
: delayMs
|
|
221
|
+
onRetry?.(error, nextAttempt, delay)
|
|
222
|
+
await promiseTimeout(delay, { signal })
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Maps with bounded concurrency; results keep input order. */
|
|
228
|
+
export async function pMap<T, R>(
|
|
229
|
+
items: Iterable<T>,
|
|
230
|
+
fn: (item: T, index: number) => Promise<R> | R,
|
|
231
|
+
options: { concurrency?: number; signal?: AbortSignal } = {}
|
|
232
|
+
): Promise<R[]> {
|
|
233
|
+
const { concurrency = Number.POSITIVE_INFINITY, signal } = options
|
|
234
|
+
if (!(concurrency >= 1)) {
|
|
235
|
+
throw new Error('Concurrency must be at least 1')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const list = [...items]
|
|
239
|
+
const results = new Array<R>(list.length)
|
|
240
|
+
let next = 0
|
|
241
|
+
|
|
242
|
+
const worker = async () => {
|
|
243
|
+
while (next < list.length) {
|
|
244
|
+
signal?.throwIfAborted()
|
|
245
|
+
const index = next++
|
|
246
|
+
results[index] = await fn(list[index]!, index)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await Promise.all(
|
|
251
|
+
Array.from({ length: Math.min(concurrency, list.length) }, worker)
|
|
252
|
+
)
|
|
253
|
+
return results
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const clock = (): number => globalThis.performance?.now() ?? Date.now()
|
|
257
|
+
|
|
258
|
+
/** Starts a clock; call the returned function to read elapsed ms. */
|
|
259
|
+
export function startTimer(): () => number {
|
|
260
|
+
const start = clock()
|
|
261
|
+
return () => clock() - start
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Runs `fn` and reports how long it took (throws pass through). */
|
|
265
|
+
export async function timed<T>(
|
|
266
|
+
fn: () => Promise<T> | T
|
|
267
|
+
): Promise<{ result: T; durationMs: number }> {
|
|
268
|
+
const elapsed = startTimer()
|
|
269
|
+
const result = await fn()
|
|
270
|
+
return { result, durationMs: elapsed() }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Aborts when any input aborts, with that signal's `reason`. Undefined inputs
|
|
275
|
+
* are skipped; no inputs gives a signal that never aborts.
|
|
276
|
+
*/
|
|
277
|
+
export function anySignal(
|
|
278
|
+
...signals: (AbortSignal | undefined)[]
|
|
279
|
+
): AbortSignal {
|
|
280
|
+
const list = signals.filter((signal) => signal !== undefined)
|
|
281
|
+
if (list.length === 1) return list[0]!
|
|
282
|
+
if (typeof AbortSignal.any === 'function') return AbortSignal.any(list)
|
|
283
|
+
|
|
284
|
+
// ponytail: fallback keeps listeners on inputs until one aborts (no WeakRef).
|
|
285
|
+
const controller = new AbortController()
|
|
286
|
+
const aborted = list.find((signal) => signal.aborted)
|
|
287
|
+
if (aborted) {
|
|
288
|
+
controller.abort(aborted.reason)
|
|
289
|
+
return controller.signal
|
|
290
|
+
}
|
|
291
|
+
const onAbort = (event: Event) => {
|
|
292
|
+
for (const signal of list) signal.removeEventListener('abort', onAbort)
|
|
293
|
+
controller.abort((event.target as AbortSignal).reason)
|
|
294
|
+
}
|
|
295
|
+
for (const signal of list) signal.addEventListener('abort', onAbort)
|
|
296
|
+
return controller.signal
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Aborts with a `TimeoutError` after `ms`, or earlier if `signal` aborts. */
|
|
300
|
+
export function timeoutSignal(ms: number, signal?: AbortSignal): AbortSignal {
|
|
301
|
+
if (typeof AbortSignal.timeout === 'function') {
|
|
302
|
+
return anySignal(AbortSignal.timeout(ms), signal)
|
|
303
|
+
}
|
|
304
|
+
const controller = new AbortController()
|
|
305
|
+
setTimeout(() => {
|
|
306
|
+
const reason = new Error('The operation timed out.')
|
|
307
|
+
reason.name = 'TimeoutError'
|
|
308
|
+
controller.abort(reason)
|
|
309
|
+
}, ms)
|
|
310
|
+
return anySignal(controller.signal, signal)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Settles like `promise`, but rejects with `signal.reason` as soon as the
|
|
315
|
+
* signal aborts. The underlying work is not cancelled.
|
|
316
|
+
*/
|
|
317
|
+
export async function raceAbort<T>(
|
|
318
|
+
promise: Promise<T>,
|
|
319
|
+
signal: AbortSignal | undefined
|
|
320
|
+
): Promise<T> {
|
|
321
|
+
if (!signal) return promise
|
|
322
|
+
let onAbort = () => {}
|
|
323
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
324
|
+
onAbort = () => reject(signal.reason)
|
|
325
|
+
if (signal.aborted) onAbort()
|
|
326
|
+
else signal.addEventListener('abort', onAbort, { once: true })
|
|
327
|
+
})
|
|
328
|
+
try {
|
|
329
|
+
return await Promise.race([aborted, promise])
|
|
330
|
+
} finally {
|
|
331
|
+
signal.removeEventListener('abort', onAbort)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export type LatestGuard = {
|
|
336
|
+
/** Starts a new generation; earlier tokens become stale. */
|
|
337
|
+
next(): number
|
|
338
|
+
isCurrent(token: number): boolean
|
|
339
|
+
/** Marks every outstanding token stale. */
|
|
340
|
+
invalidate(): void
|
|
341
|
+
/** Runs `fn` under a new token; stale results and errors become `{ stale: true }`. */
|
|
342
|
+
run<T>(
|
|
343
|
+
fn: () => Promise<T> | T
|
|
344
|
+
): Promise<{ stale: true } | { stale: false; value: T }>
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Ignores results of async work superseded by a newer call. */
|
|
348
|
+
export function createLatestGuard(): LatestGuard {
|
|
349
|
+
let current = 0
|
|
350
|
+
const isCurrent = (token: number) => token === current
|
|
351
|
+
return {
|
|
352
|
+
next: () => ++current,
|
|
353
|
+
isCurrent,
|
|
354
|
+
invalidate: () => {
|
|
355
|
+
current++
|
|
356
|
+
},
|
|
357
|
+
async run(fn) {
|
|
358
|
+
const token = ++current
|
|
359
|
+
try {
|
|
360
|
+
const value = await fn()
|
|
361
|
+
return isCurrent(token) ? { stale: false, value } : { stale: true }
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (!isCurrent(token)) return { stale: true }
|
|
364
|
+
throw error
|
|
365
|
+
}
|
|
366
|
+
}
|
|
72
367
|
}
|
|
73
368
|
}
|