effect 4.0.0-beta.23 → 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 +26 -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 +1 -1
- 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/package.json +1 -1
- package/src/Schema.ts +98 -18
- package/src/SchemaAST.ts +24 -19
- package/src/SchemaRepresentation.ts +2 -0
- package/src/ServiceMap.ts +1 -0
- package/src/internal/schema/representation.ts +1 -0
- package/src/unstable/http/HttpClient.ts +290 -2
- package/src/unstable/httpapi/HttpApi.ts +1 -1
|
@@ -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
|