effect 4.0.0-beta.22 → 4.0.0-beta.24
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/dist/Schema.d.ts +21 -0
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +61 -17
- package/dist/Schema.js.map +1 -1
- package/dist/SchemaAST.d.ts.map +1 -1
- package/dist/SchemaAST.js +30 -14
- package/dist/SchemaAST.js.map +1 -1
- package/dist/SchemaRepresentation.d.ts.map +1 -1
- package/dist/SchemaRepresentation.js +2 -0
- package/dist/SchemaRepresentation.js.map +1 -1
- package/dist/ServiceMap.d.ts +1 -0
- package/dist/ServiceMap.d.ts.map +1 -1
- package/dist/ServiceMap.js.map +1 -1
- package/dist/internal/schema/representation.js +40 -103
- package/dist/internal/schema/representation.js.map +1 -1
- package/dist/unstable/http/HttpClient.d.ts +80 -2
- package/dist/unstable/http/HttpClient.d.ts.map +1 -1
- package/dist/unstable/http/HttpClient.js +170 -0
- package/dist/unstable/http/HttpClient.js.map +1 -1
- package/dist/unstable/httpapi/HttpApi.d.ts +1 -1
- package/dist/unstable/httpapi/HttpApi.d.ts.map +1 -1
- package/dist/unstable/httpapi/OpenApi.js +1 -1
- package/dist/unstable/httpapi/OpenApi.js.map +1 -1
- package/package.json +1 -1
- package/src/Schema.ts +98 -18
- package/src/SchemaAST.ts +29 -19
- package/src/SchemaRepresentation.ts +2 -0
- package/src/ServiceMap.ts +1 -0
- package/src/internal/schema/representation.ts +37 -90
- package/src/unstable/http/HttpClient.ts +290 -2
- package/src/unstable/httpapi/HttpApi.ts +1 -1
- package/src/unstable/httpapi/OpenApi.ts +1 -1
package/src/SchemaAST.ts
CHANGED
|
@@ -2028,7 +2028,7 @@ type Type =
|
|
|
2028
2028
|
/** @internal */
|
|
2029
2029
|
export type Sentinel = {
|
|
2030
2030
|
readonly key: PropertyKey
|
|
2031
|
-
readonly literal: LiteralValue
|
|
2031
|
+
readonly literal: LiteralValue | symbol
|
|
2032
2032
|
}
|
|
2033
2033
|
|
|
2034
2034
|
function getCandidateTypes(ast: AST): ReadonlyArray<Type> {
|
|
@@ -2081,28 +2081,33 @@ function getCandidateTypes(ast: AST): ReadonlyArray<Type> {
|
|
|
2081
2081
|
}
|
|
2082
2082
|
|
|
2083
2083
|
/** @internal */
|
|
2084
|
-
export function collectSentinels(ast: AST): Array<Sentinel>
|
|
2084
|
+
export function collectSentinels(ast: AST): Array<Sentinel> {
|
|
2085
2085
|
switch (ast._tag) {
|
|
2086
|
+
default:
|
|
2087
|
+
return []
|
|
2086
2088
|
case "Declaration": {
|
|
2087
2089
|
const s = ast.annotations?.["~sentinels"]
|
|
2088
|
-
return Array.isArray(s)
|
|
2090
|
+
return Array.isArray(s) ? s : []
|
|
2089
2091
|
}
|
|
2090
|
-
case "Objects":
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2092
|
+
case "Objects":
|
|
2093
|
+
return ast.propertySignatures.flatMap((ps): Array<Sentinel> => {
|
|
2094
|
+
const type = ps.type
|
|
2095
|
+
if (!isOptional(type)) {
|
|
2096
|
+
if (isLiteral(type)) {
|
|
2097
|
+
return [{ key: ps.name, literal: type.literal }]
|
|
2098
|
+
}
|
|
2099
|
+
if (isUniqueSymbol(type)) {
|
|
2100
|
+
return [{ key: ps.name, literal: type.symbol }]
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return []
|
|
2104
|
+
})
|
|
2105
|
+
case "Arrays":
|
|
2106
|
+
return ast.elements.flatMap((e, i) => {
|
|
2107
|
+
return isLiteral(e) && !isOptional(e)
|
|
2101
2108
|
? [{ key: i, literal: e.literal }]
|
|
2102
2109
|
: []
|
|
2103
|
-
)
|
|
2104
|
-
return v.length ? v : undefined
|
|
2105
|
-
}
|
|
2110
|
+
})
|
|
2106
2111
|
case "Suspend":
|
|
2107
2112
|
return collectSentinels(ast.thunk())
|
|
2108
2113
|
}
|
|
@@ -2110,7 +2115,7 @@ export function collectSentinels(ast: AST): Array<Sentinel> | undefined {
|
|
|
2110
2115
|
|
|
2111
2116
|
type CandidateIndex = {
|
|
2112
2117
|
byType?: { [K in Type]?: Array<AST> }
|
|
2113
|
-
bySentinel?: Map<PropertyKey, Map<LiteralValue, Array<AST>>>
|
|
2118
|
+
bySentinel?: Map<PropertyKey, Map<LiteralValue | symbol, Array<AST>>>
|
|
2114
2119
|
otherwise?: { [K in Type]?: Array<AST> }
|
|
2115
2120
|
}
|
|
2116
2121
|
|
|
@@ -2132,7 +2137,7 @@ function getIndex(types: ReadonlyArray<AST>): CandidateIndex {
|
|
|
2132
2137
|
idx.byType ??= {}
|
|
2133
2138
|
for (const t of types) (idx.byType[t] ??= []).push(a)
|
|
2134
2139
|
|
|
2135
|
-
if (sentinels
|
|
2140
|
+
if (sentinels.length > 0) { // discriminated variants
|
|
2136
2141
|
idx.bySentinel ??= new Map()
|
|
2137
2142
|
for (const { key, literal } of sentinels) {
|
|
2138
2143
|
let m = idx.bySentinel.get(key)
|
|
@@ -2640,6 +2645,11 @@ export function replaceContext<A extends AST>(ast: A, context: Context | undefin
|
|
|
2640
2645
|
})
|
|
2641
2646
|
}
|
|
2642
2647
|
|
|
2648
|
+
/** @internal */
|
|
2649
|
+
export function getLastEncoding(ast: AST): AST {
|
|
2650
|
+
return ast.encoding ? getLastEncoding(ast.encoding[ast.encoding.length - 1].to) : ast
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2643
2653
|
/** @internal */
|
|
2644
2654
|
export function annotate<A extends AST>(ast: A, annotations: Schema.Annotations.Annotations): A {
|
|
2645
2655
|
if (ast.checks) {
|
|
@@ -1771,6 +1771,8 @@ export const toSchemaDefaultReviver: Reviver<Schema.Top> = (s, recur) => {
|
|
|
1771
1771
|
return Schema.Result(typeParameters[0], typeParameters[1])
|
|
1772
1772
|
case "effect/HashSet":
|
|
1773
1773
|
return Schema.HashSet(typeParameters[0])
|
|
1774
|
+
case "effect/Chunk":
|
|
1775
|
+
return Schema.Chunk(typeParameters[0])
|
|
1774
1776
|
}
|
|
1775
1777
|
}
|
|
1776
1778
|
}
|
package/src/ServiceMap.ts
CHANGED
|
@@ -321,6 +321,7 @@ export interface Reference<in out Shape> extends Service<never, Shape> {
|
|
|
321
321
|
readonly [ReferenceTypeId]: typeof ReferenceTypeId
|
|
322
322
|
readonly defaultValue: () => Shape
|
|
323
323
|
[Symbol.iterator](): EffectIterator<Reference<Shape>>
|
|
324
|
+
new(_: never): {}
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
/**
|
|
@@ -24,80 +24,13 @@ export function fromASTs(asts: readonly [AST.AST, ...Array<AST.AST>]): SchemaRep
|
|
|
24
24
|
|
|
25
25
|
const referenceMap = new Map<AST.AST, string>()
|
|
26
26
|
const uniqueReferences = new Set<string>()
|
|
27
|
-
const
|
|
27
|
+
const visiting = new Set<AST.AST>()
|
|
28
28
|
|
|
29
29
|
const schemas = Arr.map(asts, (ast) => recur(ast))
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
|
-
representations:
|
|
33
|
-
references
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isCompactable($ref: string): boolean {
|
|
37
|
-
return !usedReferences.has($ref)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function compact(s: SchemaRepresentation.Representation): SchemaRepresentation.Representation {
|
|
41
|
-
switch (s._tag) {
|
|
42
|
-
default:
|
|
43
|
-
return s
|
|
44
|
-
case "Declaration":
|
|
45
|
-
return {
|
|
46
|
-
...s,
|
|
47
|
-
typeParameters: s.typeParameters.map(compact),
|
|
48
|
-
encodedSchema: compact(s.encodedSchema)
|
|
49
|
-
}
|
|
50
|
-
case "Reference": {
|
|
51
|
-
if (isCompactable(s.$ref)) {
|
|
52
|
-
return compact(references[s.$ref])
|
|
53
|
-
}
|
|
54
|
-
return s
|
|
55
|
-
}
|
|
56
|
-
case "Suspend":
|
|
57
|
-
return { ...s, thunk: compact(s.thunk) }
|
|
58
|
-
case "String":
|
|
59
|
-
return {
|
|
60
|
-
...s,
|
|
61
|
-
...(s.contentSchema ? { contentSchema: compact(s.contentSchema) } : undefined)
|
|
62
|
-
}
|
|
63
|
-
case "TemplateLiteral":
|
|
64
|
-
return { ...s, parts: s.parts.map(compact) }
|
|
65
|
-
case "Arrays":
|
|
66
|
-
return {
|
|
67
|
-
...s,
|
|
68
|
-
elements: s.elements.map((e) => ({ ...e, type: compact(e.type) })),
|
|
69
|
-
rest: s.rest.map(compact)
|
|
70
|
-
}
|
|
71
|
-
case "Objects":
|
|
72
|
-
return {
|
|
73
|
-
...s,
|
|
74
|
-
checks: s.checks.map(compactCheck),
|
|
75
|
-
propertySignatures: s.propertySignatures.map((ps) => ({ ...ps, type: compact(ps.type) })),
|
|
76
|
-
indexSignatures: s.indexSignatures.map((is) => ({
|
|
77
|
-
...is,
|
|
78
|
-
parameter: compact(is.parameter),
|
|
79
|
-
type: compact(is.type)
|
|
80
|
-
}))
|
|
81
|
-
}
|
|
82
|
-
case "Union":
|
|
83
|
-
return { ...s, types: s.types.map(compact) }
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function compactCheck<M extends SchemaRepresentation.Meta>(
|
|
88
|
-
check: SchemaRepresentation.Check<M>
|
|
89
|
-
): SchemaRepresentation.Check<M> {
|
|
90
|
-
switch (check._tag) {
|
|
91
|
-
case "Filter":
|
|
92
|
-
return {
|
|
93
|
-
...check,
|
|
94
|
-
meta: check.meta._tag === "isPropertyNames"
|
|
95
|
-
? { _tag: "isPropertyNames", propertyNames: compact(check.meta.propertyNames) } as M
|
|
96
|
-
: check.meta
|
|
97
|
-
}
|
|
98
|
-
case "FilterGroup":
|
|
99
|
-
return { ...check, checks: Arr.map(check.checks, compactCheck) }
|
|
100
|
-
}
|
|
32
|
+
representations: schemas,
|
|
33
|
+
references
|
|
101
34
|
}
|
|
102
35
|
|
|
103
36
|
function gen(prefix: string = "_"): string {
|
|
@@ -115,32 +48,45 @@ export function fromASTs(asts: readonly [AST.AST, ...Array<AST.AST>]): SchemaRep
|
|
|
115
48
|
function recur(ast: AST.AST, prefix?: string): SchemaRepresentation.Representation {
|
|
116
49
|
const found = referenceMap.get(ast)
|
|
117
50
|
if (found !== undefined) {
|
|
118
|
-
usedReferences.add(found)
|
|
119
51
|
return { _tag: "Reference", $ref: found }
|
|
120
52
|
}
|
|
121
53
|
|
|
122
|
-
const last = getLastEncoding(ast)
|
|
123
|
-
|
|
124
|
-
if (ast === last) {
|
|
125
|
-
const reference = ast._tag === "Declaration"
|
|
126
|
-
? gen(ast._tag)
|
|
127
|
-
: gen(InternalAnnotations.resolveIdentifier(ast) ?? prefix ?? `${ast._tag}_`)
|
|
54
|
+
const last = AST.getLastEncoding(ast)
|
|
55
|
+
const identifier = InternalAnnotations.resolveIdentifier(ast) ?? prefix
|
|
128
56
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
57
|
+
if (ast !== last) {
|
|
58
|
+
return recur(last, identifier)
|
|
59
|
+
}
|
|
132
60
|
|
|
61
|
+
// Has identifier → always create reference
|
|
62
|
+
if (identifier !== undefined) {
|
|
63
|
+
const reference = gen(identifier)
|
|
133
64
|
referenceMap.set(ast, reference)
|
|
134
|
-
const out = on(ast
|
|
65
|
+
const out = on(ast)
|
|
135
66
|
references[reference] = out
|
|
136
67
|
return { _tag: "Reference", $ref: reference }
|
|
137
|
-
} else {
|
|
138
|
-
return recur(last, InternalAnnotations.resolveIdentifier(ast) ?? prefix)
|
|
139
68
|
}
|
|
140
|
-
}
|
|
141
69
|
|
|
142
|
-
|
|
143
|
-
|
|
70
|
+
// Recursion detected → create reference
|
|
71
|
+
if (visiting.has(ast)) {
|
|
72
|
+
const reference = gen(`${ast._tag}_`)
|
|
73
|
+
referenceMap.set(ast, reference)
|
|
74
|
+
return { _tag: "Reference", $ref: reference }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Normal case → inline
|
|
78
|
+
visiting.add(ast)
|
|
79
|
+
const out = on(ast)
|
|
80
|
+
visiting.delete(ast)
|
|
81
|
+
|
|
82
|
+
// A descendant triggered reference creation (recursion)
|
|
83
|
+
const ref = referenceMap.get(ast)
|
|
84
|
+
if (ref !== undefined) {
|
|
85
|
+
references[ref] = out
|
|
86
|
+
return { _tag: "Reference", $ref: ref }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return out
|
|
144
90
|
}
|
|
145
91
|
|
|
146
92
|
function getEncodedSchema(last: AST.Declaration): AST.AST {
|
|
@@ -153,12 +99,12 @@ export function fromASTs(asts: readonly [AST.AST, ...Array<AST.AST>]): SchemaRep
|
|
|
153
99
|
return AST.null
|
|
154
100
|
}
|
|
155
101
|
|
|
156
|
-
function on(last: AST.AST
|
|
102
|
+
function on(last: AST.AST): SchemaRepresentation.Representation {
|
|
157
103
|
const annotations = fromASTAnnotations(last.annotations)
|
|
158
104
|
switch (last._tag) {
|
|
159
105
|
case "Declaration": {
|
|
160
106
|
// this must be executed before transforming the type parameters
|
|
161
|
-
const encodedSchema = recur(getEncodedSchema(last)
|
|
107
|
+
const encodedSchema = recur(getEncodedSchema(last))
|
|
162
108
|
return {
|
|
163
109
|
_tag: "Declaration",
|
|
164
110
|
typeParameters: last.typeParameters.map((ast) => recur(ast)),
|
|
@@ -228,7 +174,7 @@ export function fromASTs(asts: readonly [AST.AST, ...Array<AST.AST>]): SchemaRep
|
|
|
228
174
|
return {
|
|
229
175
|
_tag: last._tag,
|
|
230
176
|
elements: last.elements.map((e) => {
|
|
231
|
-
const last = getLastEncoding(e)
|
|
177
|
+
const last = AST.getLastEncoding(e)
|
|
232
178
|
return {
|
|
233
179
|
isOptional: AST.isOptional(last),
|
|
234
180
|
type: recur(e),
|
|
@@ -243,7 +189,7 @@ export function fromASTs(asts: readonly [AST.AST, ...Array<AST.AST>]): SchemaRep
|
|
|
243
189
|
return {
|
|
244
190
|
_tag: last._tag,
|
|
245
191
|
propertySignatures: last.propertySignatures.map((ps) => {
|
|
246
|
-
const last = getLastEncoding(ps.type)
|
|
192
|
+
const last = AST.getLastEncoding(ps.type)
|
|
247
193
|
return {
|
|
248
194
|
name: ps.name,
|
|
249
195
|
type: recur(ps.type),
|
|
@@ -322,6 +268,7 @@ export function fromASTs(asts: readonly [AST.AST, ...Array<AST.AST>]): SchemaRep
|
|
|
322
268
|
export const fromASTBlacklist: Set<string> = new Set([
|
|
323
269
|
// `expected` is preserved because is useful to generate descriptions in JSON Schemas
|
|
324
270
|
"~structural",
|
|
271
|
+
"~sentinels",
|
|
325
272
|
"meta",
|
|
326
273
|
"toArbitrary",
|
|
327
274
|
"toArbitraryConstraint",
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { NonEmptyReadonlyArray } from "../../Array.ts"
|
|
5
5
|
import * as Cause from "../../Cause.ts"
|
|
6
|
+
import { Clock } from "../../Clock.ts"
|
|
7
|
+
import * as Duration from "../../Duration.ts"
|
|
6
8
|
import * as Effect from "../../Effect.ts"
|
|
7
9
|
import * as Exit from "../../Exit.ts"
|
|
8
|
-
import
|
|
10
|
+
import * as Fiber from "../../Fiber.ts"
|
|
9
11
|
import { constFalse, constTrue, dual, flow, identity } from "../../Function.ts"
|
|
10
12
|
import * as Inspectable from "../../Inspectable.ts"
|
|
11
13
|
import * as Layer from "../../Layer.ts"
|
|
@@ -19,6 +21,7 @@ import * as ServiceMap from "../../ServiceMap.ts"
|
|
|
19
21
|
import * as Stream from "../../Stream.ts"
|
|
20
22
|
import * as Tracer from "../../Tracer.ts"
|
|
21
23
|
import type { EqualsWith, ExcludeTag, ExtractTag, NoExcessProperties, NoInfer, Tags } from "../../Types.ts"
|
|
24
|
+
import type * as RateLimiter from "../persistence/RateLimiter.ts"
|
|
22
25
|
import * as Cookies from "./Cookies.ts"
|
|
23
26
|
import * as Headers from "./Headers.ts"
|
|
24
27
|
import * as Error from "./HttpClientError.ts"
|
|
@@ -630,7 +633,7 @@ export const make = (
|
|
|
630
633
|
request: HttpClientRequest.HttpClientRequest,
|
|
631
634
|
url: URL,
|
|
632
635
|
signal: AbortSignal,
|
|
633
|
-
fiber: Fiber<HttpClientResponse.HttpClientResponse, Error.HttpClientError>
|
|
636
|
+
fiber: Fiber.Fiber<HttpClientResponse.HttpClientResponse, Error.HttpClientError>
|
|
634
637
|
) => Effect.Effect<HttpClientResponse.HttpClientResponse, Error.HttpClientError>
|
|
635
638
|
): HttpClient =>
|
|
636
639
|
makeWith((effect) =>
|
|
@@ -1072,6 +1075,286 @@ export const retryTransient: {
|
|
|
1072
1075
|
}
|
|
1073
1076
|
)
|
|
1074
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* @since 4.0.0
|
|
1080
|
+
* @category rate limiting
|
|
1081
|
+
*/
|
|
1082
|
+
export declare namespace WithRateLimiter {
|
|
1083
|
+
/**
|
|
1084
|
+
* @since 4.0.0
|
|
1085
|
+
* @category rate limiting
|
|
1086
|
+
*/
|
|
1087
|
+
export interface Options {
|
|
1088
|
+
/**
|
|
1089
|
+
* The `RateLimiter` service to use for rate limiting.
|
|
1090
|
+
*/
|
|
1091
|
+
readonly limiter: RateLimiter.RateLimiter
|
|
1092
|
+
/**
|
|
1093
|
+
* The initial rate limit window duration.
|
|
1094
|
+
*/
|
|
1095
|
+
readonly window: Duration.Input
|
|
1096
|
+
/**
|
|
1097
|
+
* The initial maximum number of allowed requests in the window.
|
|
1098
|
+
*/
|
|
1099
|
+
readonly limit: number
|
|
1100
|
+
/**
|
|
1101
|
+
* The key to identify the rate limit. Requests with the same key will share
|
|
1102
|
+
* the same rate limit. This can be used to implement per-user or
|
|
1103
|
+
* per-endpoint rate limits.
|
|
1104
|
+
*/
|
|
1105
|
+
readonly key: string | ((request: HttpClientRequest.HttpClientRequest) => string)
|
|
1106
|
+
/**
|
|
1107
|
+
* Defaults to `"fixed-window"`.
|
|
1108
|
+
*/
|
|
1109
|
+
readonly algorithm?: "fixed-window" | "token-bucket" | undefined
|
|
1110
|
+
/**
|
|
1111
|
+
* Defaults to `1`.
|
|
1112
|
+
*/
|
|
1113
|
+
readonly tokens?: number | ((request: HttpClientRequest.HttpClientRequest) => number) | undefined
|
|
1114
|
+
/**
|
|
1115
|
+
* Disable automatic limits updates from response headers.
|
|
1116
|
+
*/
|
|
1117
|
+
readonly disableResponseInspection?: boolean | undefined
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Applies request rate limiting using the `RateLimiter` service.
|
|
1123
|
+
*
|
|
1124
|
+
* It can update limits by inspecting common rate limit response headers and
|
|
1125
|
+
* automatically retries HTTP `429` responses (or `HttpClientError` values
|
|
1126
|
+
* wrapping a `429` response) by forcing the retry back through the limiter.
|
|
1127
|
+
*
|
|
1128
|
+
* @since 4.0.0
|
|
1129
|
+
* @category rate limiting
|
|
1130
|
+
*/
|
|
1131
|
+
export const withRateLimiter: {
|
|
1132
|
+
/**
|
|
1133
|
+
* Applies request rate limiting using the `RateLimiter` service.
|
|
1134
|
+
*
|
|
1135
|
+
* It can update limits by inspecting common rate limit response headers and
|
|
1136
|
+
* automatically retries HTTP `429` responses (or `HttpClientError` values
|
|
1137
|
+
* wrapping a `429` response) by forcing the retry back through the limiter.
|
|
1138
|
+
*
|
|
1139
|
+
* @since 4.0.0
|
|
1140
|
+
* @category rate limiting
|
|
1141
|
+
*/
|
|
1142
|
+
(options: WithRateLimiter.Options): <E, R>(
|
|
1143
|
+
self: HttpClient.With<E, R>
|
|
1144
|
+
) => HttpClient.With<E | RateLimiter.RateLimiterError, R>
|
|
1145
|
+
/**
|
|
1146
|
+
* Applies request rate limiting using the `RateLimiter` service.
|
|
1147
|
+
*
|
|
1148
|
+
* It can update limits by inspecting common rate limit response headers and
|
|
1149
|
+
* automatically retries HTTP `429` responses (or `HttpClientError` values
|
|
1150
|
+
* wrapping a `429` response) by forcing the retry back through the limiter.
|
|
1151
|
+
*
|
|
1152
|
+
* @since 4.0.0
|
|
1153
|
+
* @category rate limiting
|
|
1154
|
+
*/
|
|
1155
|
+
<E, R>(self: HttpClient.With<E, R>, options: WithRateLimiter.Options): HttpClient.With<E | RateLimiter.RateLimiterError, R>
|
|
1156
|
+
} = dual(2, <E, R>(
|
|
1157
|
+
self: HttpClient.With<E, R>,
|
|
1158
|
+
options: WithRateLimiter.Options
|
|
1159
|
+
): HttpClient.With<E | RateLimiter.RateLimiterError, R> => {
|
|
1160
|
+
const initialState: RateLimiterState = {
|
|
1161
|
+
limit: options.limit,
|
|
1162
|
+
window: Duration.max(Duration.fromInputUnsafe(options.window), Duration.millis(1))
|
|
1163
|
+
}
|
|
1164
|
+
const states = new Map<string, RateLimiterState>()
|
|
1165
|
+
|
|
1166
|
+
const keyOption = options.key
|
|
1167
|
+
const resolveKey: (request: HttpClientRequest.HttpClientRequest) => string = typeof keyOption === "function"
|
|
1168
|
+
? keyOption
|
|
1169
|
+
: () => keyOption
|
|
1170
|
+
const tokensOption = options.tokens
|
|
1171
|
+
const resolveTokens: (request: HttpClientRequest.HttpClientRequest) => number | undefined =
|
|
1172
|
+
typeof tokensOption === "function" ? tokensOption : () => tokensOption
|
|
1173
|
+
|
|
1174
|
+
const getState = (key: string): RateLimiterState => {
|
|
1175
|
+
const current = states.get(key)
|
|
1176
|
+
if (current !== undefined) {
|
|
1177
|
+
return current
|
|
1178
|
+
}
|
|
1179
|
+
states.set(key, initialState)
|
|
1180
|
+
return initialState
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const onResponse = options.disableResponseInspection
|
|
1184
|
+
? undefined
|
|
1185
|
+
: (clock: Clock, key: string, headers: Headers.Headers, tokens: number | undefined) => {
|
|
1186
|
+
const current = getState(key)
|
|
1187
|
+
const next = parseRateLimiterState(current, clock, headers, tokens)
|
|
1188
|
+
if (next.limit !== current.limit || !Duration.equals(next.window, current.window)) {
|
|
1189
|
+
states.set(key, next)
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
return transform(self, function loop(effect, request): Effect.Effect<
|
|
1194
|
+
HttpClientResponse.HttpClientResponse,
|
|
1195
|
+
E | RateLimiter.RateLimiterError,
|
|
1196
|
+
R
|
|
1197
|
+
> {
|
|
1198
|
+
const fiber = Fiber.getCurrent()!
|
|
1199
|
+
const clock = fiber.getRef(Clock)
|
|
1200
|
+
const key = resolveKey(request)
|
|
1201
|
+
const tokens = resolveTokens(request)
|
|
1202
|
+
const current = getState(key)
|
|
1203
|
+
return Effect.flatMap(
|
|
1204
|
+
options.limiter.consume({
|
|
1205
|
+
algorithm: options.algorithm,
|
|
1206
|
+
onExceeded: "delay",
|
|
1207
|
+
key,
|
|
1208
|
+
limit: current.limit,
|
|
1209
|
+
window: current.window,
|
|
1210
|
+
tokens
|
|
1211
|
+
}),
|
|
1212
|
+
({ delay }) => {
|
|
1213
|
+
const run = Effect.matchEffect(effect, {
|
|
1214
|
+
onSuccess(response) {
|
|
1215
|
+
onResponse?.(clock, key, response.headers, tokens)
|
|
1216
|
+
return response.status === 429 ? loop(effect, request) : Effect.succeed(response)
|
|
1217
|
+
},
|
|
1218
|
+
onFailure(error) {
|
|
1219
|
+
if (isTooManyRequestsHttpClientError(error)) {
|
|
1220
|
+
onResponse?.(clock, key, error.reason.response.headers, tokens)
|
|
1221
|
+
return loop(effect, request)
|
|
1222
|
+
}
|
|
1223
|
+
return Effect.fail(error)
|
|
1224
|
+
}
|
|
1225
|
+
})
|
|
1226
|
+
return Duration.isZero(delay) ? run : Effect.delay(run, delay)
|
|
1227
|
+
}
|
|
1228
|
+
)
|
|
1229
|
+
})
|
|
1230
|
+
})
|
|
1231
|
+
|
|
1232
|
+
interface RateLimiterState {
|
|
1233
|
+
readonly limit: number
|
|
1234
|
+
readonly window: Duration.Duration
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const parseRateLimiterState = (
|
|
1238
|
+
state: RateLimiterState,
|
|
1239
|
+
clock: Clock,
|
|
1240
|
+
headers: Headers.Headers,
|
|
1241
|
+
tokens: number | undefined
|
|
1242
|
+
): RateLimiterState => {
|
|
1243
|
+
const limit = parseRateLimitLimit(headers, tokens) ?? state.limit
|
|
1244
|
+
const window = parseRateLimitWindow(clock, headers) ?? state.window
|
|
1245
|
+
if (limit === state.limit && Duration.equals(window, state.window)) {
|
|
1246
|
+
return state
|
|
1247
|
+
}
|
|
1248
|
+
return { limit, window }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
const parseRateLimitLimit = (headers: Headers.Headers, tokens: number | undefined): number | undefined => {
|
|
1252
|
+
const raw = getHeader(headers, "ratelimit-limit", "x-ratelimit-limit")
|
|
1253
|
+
const value = parseNumberHeader(raw)
|
|
1254
|
+
if (value !== undefined && value > 0) {
|
|
1255
|
+
return value
|
|
1256
|
+
}
|
|
1257
|
+
const remaining = parseRateLimitRemaining(headers)
|
|
1258
|
+
if (remaining === undefined) {
|
|
1259
|
+
return undefined
|
|
1260
|
+
}
|
|
1261
|
+
return Math.max(remaining + (tokens !== undefined && tokens > 0 ? tokens : 1), 1)
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const parseRateLimitRemaining = (headers: Headers.Headers): number | undefined => {
|
|
1265
|
+
const raw = getHeader(headers, "ratelimit-remaining", "x-ratelimit-remaining")
|
|
1266
|
+
const value = parseNumberHeader(raw)
|
|
1267
|
+
return value !== undefined && value >= 0 ? value : undefined
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const parseRateLimitWindow = (
|
|
1271
|
+
clock: Clock,
|
|
1272
|
+
headers: Headers.Headers
|
|
1273
|
+
): Duration.Duration | undefined => {
|
|
1274
|
+
const retryAfter = parseRetryAfter(
|
|
1275
|
+
clock,
|
|
1276
|
+
getHeader(headers, "retry-after")
|
|
1277
|
+
)
|
|
1278
|
+
if (retryAfter !== undefined) {
|
|
1279
|
+
return retryAfter
|
|
1280
|
+
}
|
|
1281
|
+
const resetAfter = parseResetAfter(getHeader(headers, "ratelimit-reset-after", "x-ratelimit-reset-after"))
|
|
1282
|
+
if (resetAfter !== undefined) {
|
|
1283
|
+
return resetAfter
|
|
1284
|
+
}
|
|
1285
|
+
return parseResetHeader(clock, getHeader(headers, "ratelimit-reset", "x-ratelimit-reset"))
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const parseRetryAfter = (
|
|
1289
|
+
clock: Clock,
|
|
1290
|
+
value: string | undefined
|
|
1291
|
+
): Duration.Duration | undefined => {
|
|
1292
|
+
if (value === undefined) {
|
|
1293
|
+
return undefined
|
|
1294
|
+
}
|
|
1295
|
+
const numeric = parseNumberHeader(value)
|
|
1296
|
+
if (numeric !== undefined) {
|
|
1297
|
+
return Duration.max(Duration.seconds(numeric), Duration.millis(1))
|
|
1298
|
+
}
|
|
1299
|
+
const parsedDate = Date.parse(value)
|
|
1300
|
+
if (Number.isNaN(parsedDate)) {
|
|
1301
|
+
return undefined
|
|
1302
|
+
}
|
|
1303
|
+
const millis = parsedDate - clock.currentTimeMillisUnsafe()
|
|
1304
|
+
if (millis <= 0) {
|
|
1305
|
+
return Duration.millis(1)
|
|
1306
|
+
}
|
|
1307
|
+
return Duration.millis(millis)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const parseResetAfter = (value: string | undefined): Duration.Duration | undefined => {
|
|
1311
|
+
const numeric = parseNumberHeader(value)
|
|
1312
|
+
if (numeric === undefined || numeric <= 0) {
|
|
1313
|
+
return undefined
|
|
1314
|
+
}
|
|
1315
|
+
return Duration.max(Duration.seconds(numeric), Duration.millis(1))
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const parseResetHeader = (
|
|
1319
|
+
clock: Clock,
|
|
1320
|
+
value: string | undefined
|
|
1321
|
+
): Duration.Duration | undefined => {
|
|
1322
|
+
const numeric = parseNumberHeader(value)
|
|
1323
|
+
if (numeric === undefined || numeric <= 0) {
|
|
1324
|
+
return undefined
|
|
1325
|
+
}
|
|
1326
|
+
const nowMillis = clock.currentTimeMillisUnsafe()
|
|
1327
|
+
if (numeric > 1_000_000_000_000) {
|
|
1328
|
+
return Duration.millis(Math.max(numeric - nowMillis, 1))
|
|
1329
|
+
}
|
|
1330
|
+
if (numeric > 1_000_000_000) {
|
|
1331
|
+
return Duration.millis(Math.max((numeric * 1_000) - nowMillis, 1))
|
|
1332
|
+
}
|
|
1333
|
+
return Duration.max(Duration.seconds(numeric), Duration.millis(1))
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const parseNumberHeader = (value: string | undefined): number | undefined => {
|
|
1337
|
+
if (value === undefined) {
|
|
1338
|
+
return undefined
|
|
1339
|
+
}
|
|
1340
|
+
const match = /-?\d+(?:\.\d+)?/.exec(value)
|
|
1341
|
+
if (match === null) {
|
|
1342
|
+
return undefined
|
|
1343
|
+
}
|
|
1344
|
+
const parsed = Number(match[0])
|
|
1345
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const getHeader = (headers: Headers.Headers, ...keys: Array<string>): string | undefined => {
|
|
1349
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1350
|
+
const value = headers[keys[i]]
|
|
1351
|
+
if (value !== undefined) {
|
|
1352
|
+
return value
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return undefined
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1075
1358
|
/**
|
|
1076
1359
|
* Performs an additional effect after a successful request.
|
|
1077
1360
|
*
|
|
@@ -1462,6 +1745,11 @@ const isTransientHttpError = (error: unknown) =>
|
|
|
1462
1745
|
(error.reason._tag === "TransportError" ||
|
|
1463
1746
|
(error.reason._tag === "StatusCodeError" && isTransientResponse(error.reason.response)))
|
|
1464
1747
|
|
|
1748
|
+
const isTooManyRequestsHttpClientError = (
|
|
1749
|
+
error: unknown
|
|
1750
|
+
): error is Error.HttpClientError & { readonly reason: Error.StatusCodeError } =>
|
|
1751
|
+
Error.isHttpClientError(error) && error.reason._tag === "StatusCodeError" && error.reason.response.status === 429
|
|
1752
|
+
|
|
1465
1753
|
const isTransientResponse = (response: HttpClientResponse.HttpClientResponse) =>
|
|
1466
1754
|
response.status === 408 ||
|
|
1467
1755
|
response.status === 429 ||
|
|
@@ -57,7 +57,7 @@ export interface HttpApi<
|
|
|
57
57
|
/**
|
|
58
58
|
* Prefix all endpoints in the `HttpApi`.
|
|
59
59
|
*/
|
|
60
|
-
prefix<const Prefix extends PathInput>(prefix: Prefix): HttpApi<Id, Groups
|
|
60
|
+
prefix<const Prefix extends PathInput>(prefix: Prefix): HttpApi<Id, HttpApiGroup.AddPrefix<Groups, Prefix>>
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Add a middleware to a `HttpApi`. It will be applied to all endpoints in the
|
|
@@ -363,7 +363,7 @@ export function fromApi<Id extends string, Groups extends HttpApiGroup.Any>(
|
|
|
363
363
|
|
|
364
364
|
function processParameters(schema: Schema.Top | undefined, i: OpenAPISpecParameter["in"]) {
|
|
365
365
|
if (schema) {
|
|
366
|
-
const ast = AST.
|
|
366
|
+
const ast = AST.getLastEncoding(schema.ast)
|
|
367
367
|
if (AST.isObjects(ast)) {
|
|
368
368
|
for (const ps of ast.propertySignatures) {
|
|
369
369
|
op.parameters.push({
|