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/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> | undefined {
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) && s.length ? s : undefined
2090
+ return Array.isArray(s) ? s : []
2089
2091
  }
2090
- case "Objects": {
2091
- const v = ast.propertySignatures.flatMap((ps) =>
2092
- isLiteral(ps.type) && !isOptional(ps.type)
2093
- ? [{ key: ps.name, literal: ps.type.literal }]
2094
- : []
2095
- )
2096
- return v.length ? v : undefined
2097
- }
2098
- case "Arrays": {
2099
- const v = ast.elements.flatMap((e, i) =>
2100
- isLiteral(e) && !isOptional(e)
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?.length) { // discriminated variants
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 usedReferences = new Set<string>()
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: Arr.map(schemas, compact),
33
- references: Rec.map(Rec.filter(references, (_, k) => !isCompactable(k)), compact)
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
- const encodedSchemaPrefix = ast._tag === "Declaration"
130
- ? InternalAnnotations.resolveIdentifier(ast) ?? prefix
131
- : prefix
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, encodedSchemaPrefix)
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
- function getLastEncoding(ast: AST.AST): AST.AST {
143
- return ast.encoding ? ast.encoding[ast.encoding.length - 1].to : ast
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, encodedSchemaPrefix?: string): SchemaRepresentation.Representation {
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), encodedSchemaPrefix)
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 type { Fiber } from "../../Fiber.ts"
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.toEncoded(schema.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({