effect 4.0.0-beta.87 → 4.0.0-beta.89

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.
Files changed (41) hide show
  1. package/dist/Effect.d.ts +5 -2
  2. package/dist/Effect.d.ts.map +1 -1
  3. package/dist/Effect.js +4 -1
  4. package/dist/Effect.js.map +1 -1
  5. package/dist/Latch.d.ts +19 -0
  6. package/dist/Latch.d.ts.map +1 -1
  7. package/dist/Latch.js +11 -0
  8. package/dist/Latch.js.map +1 -1
  9. package/dist/Schema.d.ts +14 -1
  10. package/dist/Schema.d.ts.map +1 -1
  11. package/dist/Schema.js +14 -1
  12. package/dist/Schema.js.map +1 -1
  13. package/dist/SchemaAST.d.ts +17 -5
  14. package/dist/SchemaAST.d.ts.map +1 -1
  15. package/dist/SchemaAST.js +22 -7
  16. package/dist/SchemaAST.js.map +1 -1
  17. package/dist/internal/effect.js +17 -15
  18. package/dist/internal/effect.js.map +1 -1
  19. package/dist/unstable/http/HttpClient.d.ts +4 -0
  20. package/dist/unstable/http/HttpClient.d.ts.map +1 -1
  21. package/dist/unstable/http/HttpClient.js +56 -20
  22. package/dist/unstable/http/HttpClient.js.map +1 -1
  23. package/dist/unstable/observability/OtlpTracer.d.ts.map +1 -1
  24. package/dist/unstable/observability/OtlpTracer.js +2 -1
  25. package/dist/unstable/observability/OtlpTracer.js.map +1 -1
  26. package/dist/unstable/persistence/RateLimiter.d.ts +106 -3
  27. package/dist/unstable/persistence/RateLimiter.d.ts.map +1 -1
  28. package/dist/unstable/persistence/RateLimiter.js +397 -5
  29. package/dist/unstable/persistence/RateLimiter.js.map +1 -1
  30. package/dist/unstable/rpc/RpcServer.js +4 -4
  31. package/dist/unstable/rpc/RpcServer.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/Effect.ts +10 -4
  34. package/src/Latch.ts +21 -0
  35. package/src/Schema.ts +14 -1
  36. package/src/SchemaAST.ts +23 -7
  37. package/src/internal/effect.ts +30 -18
  38. package/src/unstable/http/HttpClient.ts +115 -24
  39. package/src/unstable/observability/OtlpTracer.ts +2 -1
  40. package/src/unstable/persistence/RateLimiter.ts +599 -5
  41. package/src/unstable/rpc/RpcServer.ts +4 -4
@@ -1299,6 +1299,10 @@ export declare namespace WithRateLimiter {
1299
1299
  * Disable automatic limits updates from response headers.
1300
1300
  */
1301
1301
  readonly disableResponseInspection?: boolean | undefined
1302
+ /**
1303
+ * Disable adaptive learning from `Retry-After` responses.
1304
+ */
1305
+ readonly disableAdaptiveLearning?: boolean | undefined
1302
1306
  }
1303
1307
  }
1304
1308
 
@@ -1362,6 +1366,7 @@ export const withRateLimiter: {
1362
1366
  const resolveTokens: (request: HttpClientRequest.HttpClientRequest) => number = typeof tokensOption === "function"
1363
1367
  ? tokensOption
1364
1368
  : constant(tokensOption ?? 1)
1369
+ const adaptiveLearningEnabled = !options.disableAdaptiveLearning
1365
1370
 
1366
1371
  const getState = (key: string): RateLimiterState => {
1367
1372
  const current = states.get(key)
@@ -1392,13 +1397,75 @@ export const withRateLimiter: {
1392
1397
  const key = resolveKey(request)
1393
1398
  const tokens = Math.max(resolveTokens(request), 1)
1394
1399
  const current = getState(key)
1395
- function retry(response: HttpClientResponse.HttpClientResponse) {
1400
+ function retry(retryAfter: Duration.Duration | undefined) {
1396
1401
  if (options.disableResponseInspection) return loop(effect, request)
1397
- const retryAfter = parseRetryAfter(clock, getHeader(response.headers, "retry-after"))
1398
1402
  return retryAfter
1399
1403
  ? Effect.flatMap(Effect.sleep(retryAfter), () => loop(effect, request))
1400
1404
  : loop(effect, request)
1401
1405
  }
1406
+ const inspectResponse = (
1407
+ response: HttpClientResponse.HttpClientResponse,
1408
+ adaptive: RateLimiter.AdaptiveConsumeResult | undefined
1409
+ ) => {
1410
+ onResponse?.(clock, key, response.headers, tokens)
1411
+ if (options.disableResponseInspection || response.status !== 429) {
1412
+ return Effect.succeed</**
1413
+ * Applies request rate limiting using the `RateLimiter` service.
1414
+ *
1415
+ * **Details**
1416
+ *
1417
+ * It can update limits by inspecting common rate limit response headers and
1418
+ * automatically retries HTTP `429` responses (or `HttpClientError` values
1419
+ * wrapping a `429` response) by forcing the retry back through the limiter.
1420
+ *
1421
+ * @category rate limiting
1422
+ * @since 4.0.0
1423
+ */
1424
+ Duration.Duration | undefined>(undefined);
1425
+ }
1426
+ const retryAfter = parseRetryAfter(clock, getHeader(response.headers, "retry-after"))
1427
+ if (retryAfter === undefined) {
1428
+ return Effect.succeed</**
1429
+ * Applies request rate limiting using the `RateLimiter` service.
1430
+ *
1431
+ * **Details**
1432
+ *
1433
+ * It can update limits by inspecting common rate limit response headers and
1434
+ * automatically retries HTTP `429` responses (or `HttpClientError` values
1435
+ * wrapping a `429` response) by forcing the retry back through the limiter.
1436
+ *
1437
+ * @category rate limiting
1438
+ * @since 4.0.0
1439
+ */
1440
+ Duration.Duration | undefined>(undefined);
1441
+ }
1442
+ const delay = parseRateLimitWindow(clock, response.headers) ?? retryAfter
1443
+ if (adaptive === undefined) {
1444
+ return Effect.succeed</**
1445
+ * Applies request rate limiting using the `RateLimiter` service.
1446
+ *
1447
+ * **Details**
1448
+ *
1449
+ * It can update limits by inspecting common rate limit response headers and
1450
+ * automatically retries HTTP `429` responses (or `HttpClientError` values
1451
+ * wrapping a `429` response) by forcing the retry back through the limiter.
1452
+ *
1453
+ * @category rate limiting
1454
+ * @since 4.0.0
1455
+ */
1456
+ Duration.Duration | undefined>(delay);
1457
+ }
1458
+ return Effect.as(
1459
+ options.limiter.adaptiveFeedback({
1460
+ key,
1461
+ epoch: adaptive.epoch,
1462
+ tokens,
1463
+ status: response.status,
1464
+ retryAfter: delay
1465
+ }),
1466
+ delay
1467
+ )
1468
+ }
1402
1469
  return Effect.flatMap(
1403
1470
  options.limiter.consume({
1404
1471
  algorithm: options.algorithm,
@@ -1409,24 +1476,55 @@ export const withRateLimiter: {
1409
1476
  tokens
1410
1477
  }),
1411
1478
  ({ delay }) => {
1412
- const run = Effect.matchEffect(effect, {
1413
- onSuccess(response) {
1414
- onResponse?.(clock, key, response.headers, tokens)
1415
- if (response.status !== 429) return Effect.succeed(response)
1416
- return retry(response)
1417
- },
1418
- onFailure(error) {
1419
- if (isTooManyRequestsHttpClientError(error)) {
1420
- onResponse?.(clock, key, error.reason.response.headers, tokens)
1421
- return retry(error.reason.response)
1422
- }
1423
- return Effect.fail(error)
1479
+ const runAdaptive = (): Effect.Effect<
1480
+ HttpClientResponse.HttpClientResponse,
1481
+ E | RateLimiter.RateLimiterError,
1482
+ R
1483
+ > => {
1484
+ const runRequest = (adaptive: RateLimiter.AdaptiveConsumeResult | undefined) => {
1485
+ const request = Effect.matchEffect(effect, {
1486
+ onSuccess(response) {
1487
+ return Effect.flatMap(inspectResponse(response, adaptive), (retryAfter) => {
1488
+ if (response.status !== 429) return Effect.succeed(response)
1489
+ return retry(retryAfter)
1490
+ })
1491
+ },
1492
+ onFailure(error) {
1493
+ if (isTooManyRequestsHttpClientError(error)) {
1494
+ return Effect.flatMap(
1495
+ inspectResponse(error.reason.response, adaptive),
1496
+ (retryAfter) => retry(retryAfter)
1497
+ )
1498
+ }
1499
+ return Effect.fail(error)
1500
+ }
1501
+ })
1502
+ return adaptive === undefined || Duration.isZero(adaptive.delay)
1503
+ ? request
1504
+ : Effect.delay(request, adaptive.delay)
1424
1505
  }
1425
- })
1426
- return Duration.isZero(delay) ? run : Effect.delay(run, delay)
1506
+ if (!adaptiveLearningEnabled) {
1507
+ return runRequest(undefined)
1508
+ }
1509
+ return Effect.flatMap(
1510
+ options.limiter.adaptiveConsume({
1511
+ key,
1512
+ tokens,
1513
+ fallbackLimit: current.limit,
1514
+ fallbackWindow: current.window
1515
+ }),
1516
+ (adaptive) => {
1517
+ if (!Duration.isZero(adaptive.delay) && adaptive.phase === "cooldown") {
1518
+ return Effect.flatMap(Effect.sleep(adaptive.delay), runAdaptive)
1519
+ }
1520
+ return runRequest(adaptive)
1521
+ }
1522
+ )
1523
+ }
1524
+ return Duration.isZero(delay) ? runAdaptive() : Effect.flatMap(Effect.sleep(delay), runAdaptive)
1427
1525
  }
1428
1526
  )
1429
- })
1527
+ });
1430
1528
  })
1431
1529
 
1432
1530
  interface RateLimiterState {
@@ -1476,13 +1574,6 @@ const parseRateLimitWindow = (
1476
1574
  clock: Clock,
1477
1575
  headers: Headers.Headers
1478
1576
  ): Duration.Duration | undefined => {
1479
- const retryAfter = parseRetryAfter(
1480
- clock,
1481
- getHeader(headers, "retry-after")
1482
- )
1483
- if (retryAfter !== undefined) {
1484
- return retryAfter
1485
- }
1486
1577
  const resetAfter = parseResetAfter(getHeader(headers, "ratelimit-reset-after", "x-ratelimit-reset-after"))
1487
1578
  if (resetAfter !== undefined) {
1488
1579
  return resetAfter
@@ -17,6 +17,7 @@ import * as Duration from "../../Duration.ts"
17
17
  import * as Effect from "../../Effect.ts"
18
18
  import type * as Exit from "../../Exit.ts"
19
19
  import { flow } from "../../Function.ts"
20
+ import { renderPrettyError } from "../../internal/effect.ts"
20
21
  import * as Layer from "../../Layer.ts"
21
22
  import * as Option from "../../Option.ts"
22
23
  import type * as Scope from "../../Scope.ts"
@@ -311,7 +312,7 @@ const makeOtlpSpan = (self: SpanImpl): OtlpSpan => {
311
312
  {
312
313
  "key": "exception.stacktrace",
313
314
  "value": {
314
- "stringValue": error.stack ?? "No stack trace available"
315
+ "stringValue": renderPrettyError(error) ?? "No stack trace available"
315
316
  }
316
317
  }
317
318
  ]