hanzio 1.2.0 → 2.1.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 +786 -239
- package/dist/api-wrapper/builders.d.ts +4 -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 +39 -2
- 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 +4 -5
- package/dist/api-wrapper/types.d.ts +103 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +19 -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-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/index-hyt6c56h.js +290 -0
- package/dist/{src/secrets/infisical.js → index-j1ybktbr.js} +500 -538
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-ktynw3vs.js +37 -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-w882zry3.js +29 -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 +7 -0
- package/dist/secrets/index.d.ts +3 -1
- package/dist/secrets/public-config.d.ts +7 -0
- package/dist/secrets/vite.d.ts +1 -0
- package/dist/secrets/worker.d.ts +1 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1130
- 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 +739 -3145
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +50 -1428
- package/dist/src/secrets/vite.js +10 -39
- package/dist/src/secrets/worker.js +14 -273
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +340 -167
- 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 +31 -24
- 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 +78 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +32 -25
- package/src/api-wrapper/create-client.ts +171 -41
- 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 +107 -2
- package/src/api-wrapper/headers.ts +11 -6
- 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 +42 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +41 -43
- package/src/api-wrapper/types.ts +185 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +132 -147
- 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 +72 -16
- package/src/secrets/index.ts +16 -4
- package/src/secrets/public-config.ts +47 -0
- package/src/secrets/vite.ts +2 -0
- package/src/secrets/worker.ts +2 -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 +58 -38
- 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/loaders.js +0 -1529
- 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
|
}
|
package/src/secrets/core.ts
CHANGED
|
@@ -68,6 +68,15 @@ export type SecretSet<K extends string, Parsed = Record<K, string>> = {
|
|
|
68
68
|
reload: (options?: SecretRequestOptions) => Promise<SecretSet<K, Parsed>>
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** A declaration that must be loaded explicitly before its values can be read. */
|
|
72
|
+
export type LazySecretSet<
|
|
73
|
+
K extends string,
|
|
74
|
+
Parsed = Record<K, string>
|
|
75
|
+
> = SecretSet<K, Parsed> & {
|
|
76
|
+
/** Initialize once. Concurrent calls share a load; use reload to refresh. */
|
|
77
|
+
load: (options?: SecretRequestOptions) => Promise<LazySecretSet<K, Parsed>>
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
// Shared across bundled entry points and package copies in the same JS realm.
|
|
72
81
|
// Only helper-written values are registered; original environment overrides aren't.
|
|
73
82
|
const registryKey = Symbol.for('hanzio.secrets.environmentWrites.v1')
|
|
@@ -118,21 +127,19 @@ function readOverrides<K extends string>(
|
|
|
118
127
|
) as Partial<Record<K, string>>
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
export
|
|
130
|
+
export function createSecretSet<
|
|
122
131
|
const Keys extends readonly string[],
|
|
123
132
|
Parsed = Record<Keys[number], string>
|
|
124
133
|
>(
|
|
125
134
|
keys: Keys,
|
|
126
135
|
options: CustomLoaderDefineSecretSetOptions<Keys[number], Parsed>,
|
|
127
136
|
environmentVariables?: EnvironmentVariables
|
|
128
|
-
):
|
|
137
|
+
): LazySecretSet<Keys[number], Parsed> {
|
|
129
138
|
type K = Keys[number]
|
|
130
139
|
|
|
131
140
|
const keySet = [...new Set(keys)] as K[]
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
environmentVariables
|
|
135
|
-
)
|
|
141
|
+
let environment: string | undefined
|
|
142
|
+
let initialized = false
|
|
136
143
|
const logger =
|
|
137
144
|
options.logger === false ? undefined : (options.logger ?? defaultLogger)
|
|
138
145
|
|
|
@@ -157,7 +164,8 @@ export async function defineSecretSet<
|
|
|
157
164
|
// Build, validate, and commit one snapshot under the caller’s deadline.
|
|
158
165
|
const loadSnapshot = async (
|
|
159
166
|
request: SecretRequestOptions,
|
|
160
|
-
signal: AbortSignal
|
|
167
|
+
signal: AbortSignal,
|
|
168
|
+
environment: string
|
|
161
169
|
) => {
|
|
162
170
|
// Fetch only keys that lack genuine local overrides.
|
|
163
171
|
const local = readOverrides(keySet, environmentVariables)
|
|
@@ -261,6 +269,7 @@ export async function defineSecretSet<
|
|
|
261
269
|
cached = next
|
|
262
270
|
parsed = nextParsed
|
|
263
271
|
sources = nextSources
|
|
272
|
+
initialized = true
|
|
264
273
|
}
|
|
265
274
|
|
|
266
275
|
const load = (request: SecretRequestOptions = {}): Promise<void> => {
|
|
@@ -273,12 +282,18 @@ export async function defineSecretSet<
|
|
|
273
282
|
const start = Date.now()
|
|
274
283
|
|
|
275
284
|
try {
|
|
285
|
+
environment ??= resolveSecretEnvironment(
|
|
286
|
+
options.environment,
|
|
287
|
+
environmentVariables
|
|
288
|
+
)
|
|
289
|
+
const resolvedEnvironment = environment
|
|
290
|
+
|
|
276
291
|
await withSecretDeadline(
|
|
277
292
|
{
|
|
278
293
|
timeoutMs: request.timeoutMs ?? options.timeoutMs,
|
|
279
294
|
signal: request.signal ?? options.signal
|
|
280
295
|
},
|
|
281
|
-
(signal) => loadSnapshot(request, signal)
|
|
296
|
+
(signal) => loadSnapshot(request, signal, resolvedEnvironment)
|
|
282
297
|
)
|
|
283
298
|
|
|
284
299
|
// Report the sources of committed values, not attempted fetches.
|
|
@@ -293,7 +308,7 @@ export async function defineSecretSet<
|
|
|
293
308
|
const counts = Object.fromEntries(sourceCounts)
|
|
294
309
|
|
|
295
310
|
log('info', 'Loaded secrets', {
|
|
296
|
-
environment,
|
|
311
|
+
environment: resolvedEnvironment,
|
|
297
312
|
count: keySet.length,
|
|
298
313
|
durationMs: Date.now() - start,
|
|
299
314
|
source: Object.keys(counts).join(' + ') || 'none',
|
|
@@ -306,7 +321,7 @@ export async function defineSecretSet<
|
|
|
306
321
|
: new SecretLoadError('LOADER', 'Secret loading failed.')
|
|
307
322
|
|
|
308
323
|
log('error', 'Secret loading failed', {
|
|
309
|
-
environment,
|
|
324
|
+
environment: environment ?? 'unresolved',
|
|
310
325
|
count: keySet.length,
|
|
311
326
|
durationMs: Date.now() - start,
|
|
312
327
|
code: safe.code
|
|
@@ -321,8 +336,19 @@ export async function defineSecretSet<
|
|
|
321
336
|
return inFlight
|
|
322
337
|
}
|
|
323
338
|
|
|
324
|
-
const
|
|
339
|
+
const assertLoaded = () => {
|
|
340
|
+
if (!initialized) {
|
|
341
|
+
throw new SecretLoadError(
|
|
342
|
+
'CONFIGURATION',
|
|
343
|
+
'Secret set has not been loaded. Await load() before reading values.'
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const secretSet: LazySecretSet<K, Parsed> = {
|
|
325
349
|
secret(key) {
|
|
350
|
+
assertLoaded()
|
|
351
|
+
|
|
326
352
|
if (!Object.hasOwn(cached, key)) {
|
|
327
353
|
throw new SecretLoadError('MISSING_SECRETS', `Secret ${key} not found`)
|
|
328
354
|
}
|
|
@@ -331,9 +357,29 @@ export async function defineSecretSet<
|
|
|
331
357
|
},
|
|
332
358
|
|
|
333
359
|
// Return copies so consumers cannot mutate the internal snapshots.
|
|
334
|
-
secrets
|
|
335
|
-
|
|
336
|
-
|
|
360
|
+
secrets() {
|
|
361
|
+
assertLoaded()
|
|
362
|
+
|
|
363
|
+
return { ...cached }
|
|
364
|
+
},
|
|
365
|
+
parsed() {
|
|
366
|
+
assertLoaded()
|
|
367
|
+
|
|
368
|
+
return structuredClone(parsed)
|
|
369
|
+
},
|
|
370
|
+
sources() {
|
|
371
|
+
assertLoaded()
|
|
372
|
+
|
|
373
|
+
return { ...sources }
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async load(request) {
|
|
377
|
+
if (!initialized) {
|
|
378
|
+
await load(request)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return secretSet
|
|
382
|
+
},
|
|
337
383
|
|
|
338
384
|
async reload(request) {
|
|
339
385
|
await load(request)
|
|
@@ -342,7 +388,17 @@ export async function defineSecretSet<
|
|
|
342
388
|
}
|
|
343
389
|
}
|
|
344
390
|
|
|
345
|
-
await load()
|
|
346
|
-
|
|
347
391
|
return secretSet
|
|
348
392
|
}
|
|
393
|
+
|
|
394
|
+
/** Compatibility API: declares and loads a complete snapshot before returning. */
|
|
395
|
+
export async function defineSecretSet<
|
|
396
|
+
const Keys extends readonly string[],
|
|
397
|
+
Parsed = Record<Keys[number], string>
|
|
398
|
+
>(
|
|
399
|
+
keys: Keys,
|
|
400
|
+
options: CustomLoaderDefineSecretSetOptions<Keys[number], Parsed>,
|
|
401
|
+
environmentVariables?: EnvironmentVariables
|
|
402
|
+
): Promise<SecretSet<Keys[number], Parsed>> {
|
|
403
|
+
return createSecretSet(keys, options, environmentVariables).load()
|
|
404
|
+
}
|
package/src/secrets/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
createSecretSet as createCoreSecretSet,
|
|
3
|
+
type LazySecretSet,
|
|
3
4
|
type CommonSecretSetOptions,
|
|
4
5
|
type CustomLoaderDefineSecretSetOptions,
|
|
5
6
|
type SecretSet,
|
|
@@ -51,13 +52,13 @@ export function getSecretEnvironment(): SecretEnvironment {
|
|
|
51
52
|
) as SecretEnvironment
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
export
|
|
55
|
+
export function createSecretSet<
|
|
55
56
|
const Keys extends readonly string[],
|
|
56
57
|
Parsed = Record<Keys[number], string>
|
|
57
58
|
>(
|
|
58
59
|
keys: Keys,
|
|
59
60
|
options: DefineSecretSetOptions<Keys[number], Parsed>
|
|
60
|
-
):
|
|
61
|
+
): LazySecretSet<Keys[number], Parsed> {
|
|
61
62
|
let loader: SecretSetLoader<Keys[number]>
|
|
62
63
|
|
|
63
64
|
if (!options.loader) {
|
|
@@ -69,7 +70,7 @@ export async function defineSecretSet<
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
// Supply process.env only from this Node/Bun entry point, keeping the core portable.
|
|
72
|
-
return
|
|
73
|
+
return createCoreSecretSet(
|
|
73
74
|
keys,
|
|
74
75
|
{ ...options, loader },
|
|
75
76
|
getProcessEnvironment()
|
|
@@ -92,3 +93,14 @@ function createDefaultInfisicalLoader<K extends string, Parsed>(
|
|
|
92
93
|
options.clientSecretEnvKey ?? 'INFISICAL_CLIENT_SECRET'
|
|
93
94
|
})
|
|
94
95
|
}
|
|
96
|
+
|
|
97
|
+
/** Compatibility API: load immediately and fail before application startup. */
|
|
98
|
+
export async function defineSecretSet<
|
|
99
|
+
const Keys extends readonly string[],
|
|
100
|
+
Parsed = Record<Keys[number], string>
|
|
101
|
+
>(
|
|
102
|
+
keys: Keys,
|
|
103
|
+
options: DefineSecretSetOptions<Keys[number], Parsed>
|
|
104
|
+
): Promise<SecretSet<Keys[number], Parsed>> {
|
|
105
|
+
return createSecretSet(keys, options).load()
|
|
106
|
+
}
|