@effect/platform 0.69.27 → 0.69.29

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 (59) hide show
  1. package/dist/cjs/HttpApi.js +33 -0
  2. package/dist/cjs/HttpApi.js.map +1 -1
  3. package/dist/cjs/HttpApiBuilder.js +23 -5
  4. package/dist/cjs/HttpApiBuilder.js.map +1 -1
  5. package/dist/cjs/HttpApiClient.js +64 -6
  6. package/dist/cjs/HttpApiClient.js.map +1 -1
  7. package/dist/cjs/HttpApiEndpoint.js.map +1 -1
  8. package/dist/cjs/HttpClient.js +4 -1
  9. package/dist/cjs/HttpClient.js.map +1 -1
  10. package/dist/cjs/OpenApi.js +12 -7
  11. package/dist/cjs/OpenApi.js.map +1 -1
  12. package/dist/cjs/PlatformLogger.js +2 -0
  13. package/dist/cjs/PlatformLogger.js.map +1 -1
  14. package/dist/cjs/UrlParams.js +35 -1
  15. package/dist/cjs/UrlParams.js.map +1 -1
  16. package/dist/cjs/internal/httpClient.js +4 -1
  17. package/dist/cjs/internal/httpClient.js.map +1 -1
  18. package/dist/dts/HttpApi.d.ts +5 -0
  19. package/dist/dts/HttpApi.d.ts.map +1 -1
  20. package/dist/dts/HttpApiBuilder.d.ts +2 -3
  21. package/dist/dts/HttpApiBuilder.d.ts.map +1 -1
  22. package/dist/dts/HttpApiClient.d.ts.map +1 -1
  23. package/dist/dts/HttpApiEndpoint.d.ts +3 -1
  24. package/dist/dts/HttpApiEndpoint.d.ts.map +1 -1
  25. package/dist/dts/HttpClient.d.ts +15 -3
  26. package/dist/dts/HttpClient.d.ts.map +1 -1
  27. package/dist/dts/OpenApi.d.ts.map +1 -1
  28. package/dist/dts/PlatformLogger.d.ts +6 -0
  29. package/dist/dts/PlatformLogger.d.ts.map +1 -1
  30. package/dist/dts/UrlParams.d.ts +24 -0
  31. package/dist/dts/UrlParams.d.ts.map +1 -1
  32. package/dist/dts/internal/httpClient.d.ts.map +1 -1
  33. package/dist/esm/HttpApi.js +33 -0
  34. package/dist/esm/HttpApi.js.map +1 -1
  35. package/dist/esm/HttpApiBuilder.js +23 -5
  36. package/dist/esm/HttpApiBuilder.js.map +1 -1
  37. package/dist/esm/HttpApiClient.js +64 -6
  38. package/dist/esm/HttpApiClient.js.map +1 -1
  39. package/dist/esm/HttpApiEndpoint.js.map +1 -1
  40. package/dist/esm/HttpClient.js +4 -1
  41. package/dist/esm/HttpClient.js.map +1 -1
  42. package/dist/esm/OpenApi.js +12 -7
  43. package/dist/esm/OpenApi.js.map +1 -1
  44. package/dist/esm/PlatformLogger.js +2 -0
  45. package/dist/esm/PlatformLogger.js.map +1 -1
  46. package/dist/esm/UrlParams.js +33 -0
  47. package/dist/esm/UrlParams.js.map +1 -1
  48. package/dist/esm/internal/httpClient.js +4 -1
  49. package/dist/esm/internal/httpClient.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/HttpApi.ts +45 -0
  52. package/src/HttpApiBuilder.ts +28 -17
  53. package/src/HttpApiClient.ts +74 -13
  54. package/src/HttpApiEndpoint.ts +2 -1
  55. package/src/HttpClient.ts +23 -9
  56. package/src/OpenApi.ts +8 -12
  57. package/src/PlatformLogger.ts +6 -0
  58. package/src/UrlParams.ts +34 -0
  59. package/src/internal/httpClient.ts +16 -4
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import * as Cause from "effect/Cause"
4
5
  import * as Chunk from "effect/Chunk"
5
6
  import * as Context from "effect/Context"
6
7
  import * as Effect from "effect/Effect"
@@ -38,6 +39,7 @@ import * as HttpServerRequest from "./HttpServerRequest.js"
38
39
  import * as HttpServerResponse from "./HttpServerResponse.js"
39
40
  import * as OpenApi from "./OpenApi.js"
40
41
  import type { Path } from "./Path.js"
42
+ import * as UrlParams from "./UrlParams.js"
41
43
 
42
44
  /**
43
45
  * The router that the API endpoints are attached to.
@@ -111,13 +113,13 @@ export const httpApp: Effect.Effect<
111
113
  const encodeError = Schema.encodeUnknown(errorSchema)
112
114
  return router.pipe(
113
115
  apiMiddleware,
114
- Effect.catchAll((error) =>
115
- Effect.matchEffect(Effect.provide(encodeError(error), context), {
116
- onFailure: () => Effect.die(error),
116
+ Effect.catchAllCause((cause) =>
117
+ Effect.matchEffect(Effect.provide(encodeError(Cause.squash(cause)), context), {
118
+ onFailure: () => Effect.failCause(cause),
117
119
  onSuccess: Effect.succeed
118
120
  })
119
121
  )
120
- )
122
+ ) as any
121
123
  })
122
124
 
123
125
  /**
@@ -126,6 +128,7 @@ export const httpApp: Effect.Effect<
126
128
  * @since 1.0.0
127
129
  * @category constructors
128
130
  * @example
131
+ * ```ts
129
132
  * import { HttpApi, HttpApiBuilder, HttpServer } from "@effect/platform"
130
133
  * import { Layer } from "effect"
131
134
  *
@@ -141,6 +144,7 @@ export const httpApp: Effect.Effect<
141
144
  * HttpServer.layerContext
142
145
  * )
143
146
  * )
147
+ * ```
144
148
  */
145
149
  export const toWebHandler = <LA, LE>(
146
150
  layer: Layer.Layer<LA | HttpApi.Api | HttpRouter.HttpRouter.DefaultServices, LE>,
@@ -506,20 +510,31 @@ export const handler = <
506
510
 
507
511
  const requestPayload = (
508
512
  request: HttpServerRequest.HttpServerRequest,
509
- urlParams: ReadonlyRecord<string, string | Array<string>>,
510
- isMultipart: boolean
513
+ urlParams: ReadonlyRecord<string, string | Array<string>>
511
514
  ): Effect.Effect<
512
515
  unknown,
513
516
  never,
514
517
  | FileSystem
515
518
  | Path
516
519
  | Scope
517
- > =>
518
- HttpMethod.hasBody(request.method)
519
- ? isMultipart
520
- ? Effect.orDie(request.multipart)
521
- : Effect.orDie(request.json)
522
- : Effect.succeed(urlParams)
520
+ > => {
521
+ if (!HttpMethod.hasBody(request.method)) {
522
+ return Effect.succeed(urlParams)
523
+ }
524
+ const contentType = request.headers["content-type"]
525
+ ? request.headers["content-type"].toLowerCase().trim()
526
+ : "application/json"
527
+ if (contentType.includes("application/json")) {
528
+ return Effect.orDie(request.json)
529
+ } else if (contentType.includes("multipart/form-data")) {
530
+ return Effect.orDie(request.multipart)
531
+ } else if (contentType.includes("x-www-form-urlencoded")) {
532
+ return Effect.map(Effect.orDie(request.urlParamsBody), UrlParams.toRecord)
533
+ } else if (contentType.startsWith("text/")) {
534
+ return Effect.orDie(request.text)
535
+ }
536
+ return Effect.map(Effect.orDie(request.arrayBuffer), (buffer) => new Uint8Array(buffer))
537
+ }
523
538
 
524
539
  type MiddlewareMap = Map<string, {
525
540
  readonly tag: HttpApiMiddleware.TagClassAny
@@ -552,10 +567,6 @@ const handlerToRoute = (
552
567
  ): HttpRouter.Route<any, any> => {
553
568
  const endpoint = endpoint_ as HttpApiEndpoint.HttpApiEndpoint.AnyWithProps
554
569
  const decodePath = Option.map(endpoint.pathSchema, Schema.decodeUnknown)
555
- const isMultipart = endpoint.payloadSchema.pipe(
556
- Option.map((schema) => HttpApiSchema.getMultipart(schema.ast)),
557
- Option.getOrElse(() => false)
558
- )
559
570
  const decodePayload = Option.map(endpoint.payloadSchema, Schema.decodeUnknown)
560
571
  const decodeHeaders = Option.map(endpoint.headersSchema, Schema.decodeUnknown)
561
572
  const decodeUrlParams = Option.map(endpoint.urlParamsSchema, Schema.decodeUnknown)
@@ -577,7 +588,7 @@ const handlerToRoute = (
577
588
  }
578
589
  if (decodePayload._tag === "Some") {
579
590
  request.payload = yield* Effect.flatMap(
580
- requestPayload(httpRequest, urlParams, isMultipart),
591
+ requestPayload(httpRequest, urlParams),
581
592
  decodePayload.value
582
593
  )
583
594
  }
@@ -4,6 +4,7 @@
4
4
  import * as Context from "effect/Context"
5
5
  import * as Effect from "effect/Effect"
6
6
  import { identity } from "effect/Function"
7
+ import { globalValue } from "effect/GlobalValue"
7
8
  import * as Option from "effect/Option"
8
9
  import * as ParseResult from "effect/ParseResult"
9
10
  import type * as Predicate from "effect/Predicate"
@@ -15,12 +16,14 @@ import * as HttpApi from "./HttpApi.js"
15
16
  import type { HttpApiEndpoint } from "./HttpApiEndpoint.js"
16
17
  import type { HttpApiGroup } from "./HttpApiGroup.js"
17
18
  import * as HttpApiSchema from "./HttpApiSchema.js"
19
+ import * as HttpBody from "./HttpBody.js"
18
20
  import * as HttpClient from "./HttpClient.js"
19
21
  import * as HttpClientError from "./HttpClientError.js"
20
22
  import * as HttpClientRequest from "./HttpClientRequest.js"
21
23
  import * as HttpClientResponse from "./HttpClientResponse.js"
22
24
  import * as HttpMethod from "./HttpMethod.js"
23
25
  import type { HttpApiMiddleware } from "./index.js"
26
+ import * as UrlParams from "./UrlParams.js"
24
27
 
25
28
  /**
26
29
  * @since 1.0.0
@@ -160,13 +163,13 @@ const makeClient = <Groups extends HttpApiGroup.Any, ApiError, ApiR>(
160
163
  successes.forEach((ast, status) => {
161
164
  decodeMap[status] = ast._tag === "None" ? responseAsVoid : schemaToResponse(ast.value)
162
165
  })
163
- const isMultipart = endpoint.payloadSchema.pipe(
164
- Option.map((schema) => HttpApiSchema.getMultipart(schema.ast)),
165
- Option.getOrElse(() => false)
166
- )
167
- const encodePayload = endpoint.payloadSchema.pipe(
168
- Option.filter(() => !isMultipart),
169
- Option.map(Schema.encodeUnknown)
166
+ const encodePayloadBody = endpoint.payloadSchema.pipe(
167
+ Option.map((schema) => {
168
+ if (HttpMethod.hasBody(endpoint.method)) {
169
+ return Schema.encodeUnknown(payloadSchemaBody(schema as any))
170
+ }
171
+ return Schema.encodeUnknown(schema)
172
+ })
170
173
  )
171
174
  const encodeHeaders = endpoint.headersSchema.pipe(
172
175
  Option.map(Schema.encodeUnknown)
@@ -185,13 +188,16 @@ const makeClient = <Groups extends HttpApiGroup.Any, ApiError, ApiR>(
185
188
  let httpRequest = HttpClientRequest.make(endpoint.method)(
186
189
  request && request.path ? makeUrl(request.path) : endpoint.path
187
190
  )
188
- if (isMultipart) {
191
+ if (request && request.payload instanceof FormData) {
189
192
  httpRequest = HttpClientRequest.bodyFormData(httpRequest, request.payload)
190
- } else if (encodePayload._tag === "Some") {
191
- const payload = yield* encodePayload.value(request.payload)
192
- httpRequest = HttpMethod.hasBody(endpoint.method)
193
- ? yield* Effect.orDie(HttpClientRequest.bodyJson(httpRequest, payload))
194
- : HttpClientRequest.setUrlParams(httpRequest, payload as any)
193
+ } else if (encodePayloadBody._tag === "Some") {
194
+ if (HttpMethod.hasBody(endpoint.method)) {
195
+ const body = (yield* encodePayloadBody.value(request.payload)) as HttpBody.HttpBody
196
+ httpRequest = HttpClientRequest.setBody(httpRequest, body)
197
+ } else {
198
+ const urlParams = (yield* encodePayloadBody.value(request.payload)) as Record<string, string>
199
+ httpRequest = HttpClientRequest.setUrlParams(httpRequest, urlParams)
200
+ }
195
201
  }
196
202
  if (encodeHeaders._tag === "Some") {
197
203
  httpRequest = HttpClientRequest.setHeaders(
@@ -421,3 +427,58 @@ const statusCodeError = (response: HttpClientResponse.HttpClientResponse) =>
421
427
  )
422
428
 
423
429
  const responseAsVoid = (_response: HttpClientResponse.HttpClientResponse) => Effect.void
430
+
431
+ const HttpBodyFromSelf = Schema.declare(HttpBody.isHttpBody)
432
+
433
+ const payloadSchemaBody = (schema: Schema.Schema.All): Schema.Schema<any, HttpBody.HttpBody> => {
434
+ const members = schema.ast._tag === "Union" ? schema.ast.types : [schema.ast]
435
+ return Schema.Union(...members.map(bodyFromPayload)) as any
436
+ }
437
+
438
+ const bodyFromPayloadCache = globalValue(
439
+ "@effect/platform/HttpApiClient/bodyFromPayloadCache",
440
+ () => new WeakMap<AST.AST, Schema.Schema.Any>()
441
+ )
442
+
443
+ const bodyFromPayload = (ast: AST.AST) => {
444
+ if (bodyFromPayloadCache.has(ast)) {
445
+ return bodyFromPayloadCache.get(ast)!
446
+ }
447
+ const schema = Schema.make(ast)
448
+ const encoding = HttpApiSchema.getEncoding(ast)
449
+ const transform = Schema.transformOrFail(
450
+ HttpBodyFromSelf,
451
+ schema,
452
+ {
453
+ decode(fromA, _, ast) {
454
+ return ParseResult.fail(new ParseResult.Forbidden(ast, fromA, "encode only schema"))
455
+ },
456
+ encode(toI, _, ast) {
457
+ switch (encoding.kind) {
458
+ case "Json": {
459
+ return HttpBody.json(toI).pipe(
460
+ ParseResult.mapError((error) => new ParseResult.Type(ast, toI, `Could not encode as JSON: ${error}`))
461
+ )
462
+ }
463
+ case "Text": {
464
+ if (typeof toI !== "string") {
465
+ return ParseResult.fail(new ParseResult.Type(ast, toI, "Expected a string"))
466
+ }
467
+ return ParseResult.succeed(HttpBody.text(toI))
468
+ }
469
+ case "UrlParams": {
470
+ return ParseResult.succeed(HttpBody.urlParams(UrlParams.fromInput(toI as any)))
471
+ }
472
+ case "Uint8Array": {
473
+ if (!(toI instanceof Uint8Array)) {
474
+ return ParseResult.fail(new ParseResult.Type(ast, toI, "Expected a Uint8Array"))
475
+ }
476
+ return ParseResult.succeed(HttpBody.uint8Array(toI))
477
+ }
478
+ }
479
+ }
480
+ }
481
+ )
482
+ bodyFromPayloadCache.set(ast, transform)
483
+ return transform
484
+ }
@@ -406,7 +406,8 @@ export declare namespace HttpApiEndpoint {
406
406
  & ([UrlParams] extends [never] ? {} : { readonly urlParams: UrlParams })
407
407
  & ([Headers] extends [never] ? {} : { readonly headers: Headers })
408
408
  & ([Payload] extends [never] ? {}
409
- : [Payload] extends [Brand<HttpApiSchema.MultipartTypeId>] ? { readonly payload: FormData }
409
+ : Payload extends infer P ?
410
+ P extends Brand<HttpApiSchema.MultipartTypeId> ? { readonly payload: FormData } : { readonly payload: P }
410
411
  : { readonly payload: Payload })
411
412
  ) extends infer Req ? keyof Req extends never ? (void | { readonly withResponse?: WithResponse }) :
412
413
  Req & { readonly withResponse?: WithResponse } :
package/src/HttpClient.ts CHANGED
@@ -12,6 +12,7 @@ import type * as Predicate from "effect/Predicate"
12
12
  import type { Ref } from "effect/Ref"
13
13
  import type * as Schedule from "effect/Schedule"
14
14
  import type * as Scope from "effect/Scope"
15
+ import type { NoInfer } from "effect/Types"
15
16
  import type { Cookies } from "./Cookies.js"
16
17
  import type * as Error from "./HttpClientError.js"
17
18
  import type * as ClientRequest from "./HttpClientRequest.js"
@@ -636,34 +637,47 @@ export const retry: {
636
637
  } = internal.retry
637
638
 
638
639
  /**
639
- * Retries common transient errors, such as rate limiting or network issues.
640
+ * Retries common transient errors, such as rate limiting, timeouts or network issues.
641
+ *
642
+ * Specifying a `while` predicate allows you to consider other errors as
643
+ * transient.
640
644
  *
641
645
  * @since 1.0.0
642
646
  * @category error handling
643
647
  */
644
648
  export const retryTransient: {
645
649
  /**
646
- * Retries common transient errors, such as rate limiting or network issues.
650
+ * Retries common transient errors, such as rate limiting, timeouts or network issues.
651
+ *
652
+ * Specifying a `while` predicate allows you to consider other errors as
653
+ * transient.
647
654
  *
648
655
  * @since 1.0.0
649
656
  * @category error handling
650
657
  */
651
658
  <B, E, R1 = never>(
652
- options:
653
- | { readonly schedule?: Schedule.Schedule<B, NoInfer<E>, R1>; readonly times?: number }
654
- | Schedule.Schedule<B, NoInfer<E>, R1>
659
+ options: {
660
+ readonly while?: Predicate.Predicate<NoInfer<E>>
661
+ readonly schedule?: Schedule.Schedule<B, NoInfer<E>, R1>
662
+ readonly times?: number
663
+ } | Schedule.Schedule<B, NoInfer<E>, R1>
655
664
  ): <R>(self: HttpClient<E, R>) => HttpClient<E, R1 | R>
656
665
  /**
657
- * Retries common transient errors, such as rate limiting or network issues.
666
+ * Retries common transient errors, such as rate limiting, timeouts or network issues.
667
+ *
668
+ * Specifying a `while` predicate allows you to consider other errors as
669
+ * transient.
658
670
  *
659
671
  * @since 1.0.0
660
672
  * @category error handling
661
673
  */
662
674
  <E, R, B, R1 = never>(
663
675
  self: HttpClient<E, R>,
664
- options:
665
- | { readonly schedule?: Schedule.Schedule<B, NoInfer<E>, R1>; readonly times?: number }
666
- | Schedule.Schedule<B, NoInfer<E>, R1>
676
+ options: {
677
+ readonly while?: Predicate.Predicate<NoInfer<E>>
678
+ readonly schedule?: Schedule.Schedule<B, NoInfer<E>, R1>
679
+ readonly times?: number
680
+ } | Schedule.Schedule<B, NoInfer<E>, R1>
667
681
  ): HttpClient<E, R1 | R>
668
682
  } = internal.retryTransient
669
683
 
package/src/OpenApi.ts CHANGED
@@ -231,7 +231,7 @@ export const fromApi = <A extends HttpApi.HttpApi.Any>(self: A): OpenAPISpec =>
231
231
  })
232
232
  spec.tags!.push(tag)
233
233
  },
234
- onEndpoint({ endpoint, errors, group, middleware, successes }) {
234
+ onEndpoint({ endpoint, errors, group, middleware, payloads, successes }) {
235
235
  const path = endpoint.path.replace(/:(\w+)[^/]*/g, "{$1}")
236
236
  const method = endpoint.method.toLowerCase() as OpenAPISpecMethodName
237
237
  const op: DeepMutable<OpenAPISpecOperation> = {
@@ -266,19 +266,15 @@ export const fromApi = <A extends HttpApi.HttpApi.Any>(self: A): OpenAPISpec =>
266
266
  op.security!.push({ [name]: [] })
267
267
  }
268
268
  })
269
- endpoint.payloadSchema.pipe(
270
- Option.filter(() => HttpMethod.hasBody(endpoint.method)),
271
- Option.map((schema) => {
272
- op.requestBody = {
273
- content: {
274
- [HttpApiSchema.getMultipart(schema.ast) ? "multipart/form-data" : "application/json"]: {
275
- schema: makeJsonSchemaOrRef(schema)
276
- }
277
- },
278
- required: true
269
+ if (payloads.size > 0) {
270
+ const content: Mutable<OpenApiSpecContent> = {}
271
+ payloads.forEach(({ ast }, contentType) => {
272
+ content[contentType as OpenApiSpecContentType] = {
273
+ schema: makeJsonSchemaOrRef(Schema.make(ast))
279
274
  }
280
275
  })
281
- )
276
+ op.requestBody = { content, required: true }
277
+ }
282
278
  for (const [status, ast] of successes) {
283
279
  if (op.responses![status]) continue
284
280
  op.responses![status] = {
@@ -14,6 +14,7 @@ import * as internal from "./internal/platformLogger.js"
14
14
  *
15
15
  * @since 1.0.0
16
16
  * @example
17
+ * ```ts
17
18
  * import { PlatformLogger } from "@effect/platform"
18
19
  * import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
19
20
  * import { Effect, Layer, Logger } from "effect"
@@ -31,6 +32,7 @@ import * as internal from "./internal/platformLogger.js"
31
32
  * Effect.provide(LoggerLive),
32
33
  * NodeRuntime.runMain
33
34
  * )
35
+ * ```
34
36
  */
35
37
  export const toFile: {
36
38
  /**
@@ -38,6 +40,7 @@ export const toFile: {
38
40
  *
39
41
  * @since 1.0.0
40
42
  * @example
43
+ * ```ts
41
44
  * import { PlatformLogger } from "@effect/platform"
42
45
  * import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
43
46
  * import { Effect, Layer, Logger } from "effect"
@@ -55,6 +58,7 @@ export const toFile: {
55
58
  * Effect.provide(LoggerLive),
56
59
  * NodeRuntime.runMain
57
60
  * )
61
+ * ```
58
62
  */
59
63
  (
60
64
  path: string,
@@ -73,6 +77,7 @@ export const toFile: {
73
77
  *
74
78
  * @since 1.0.0
75
79
  * @example
80
+ * ```ts
76
81
  * import { PlatformLogger } from "@effect/platform"
77
82
  * import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
78
83
  * import { Effect, Layer, Logger } from "effect"
@@ -90,6 +95,7 @@ export const toFile: {
90
95
  * Effect.provide(LoggerLive),
91
96
  * NodeRuntime.runMain
92
97
  * )
98
+ * ```
93
99
  */
94
100
  <Message>(
95
101
  self: Logger.Logger<Message, string>,
package/src/UrlParams.ts CHANGED
@@ -274,6 +274,40 @@ const baseUrl = (): string | undefined => {
274
274
  return undefined
275
275
  }
276
276
 
277
+ /**
278
+ * Builds a `Record` containing all the key-value pairs in the given `UrlParams`
279
+ * as `string` (if only one value for a key) or a `NonEmptyArray<string>`
280
+ * (when more than one value for a key)
281
+ *
282
+ * @example
283
+ * import { UrlParams } from "@effect/platform"
284
+ *
285
+ * const urlParams = UrlParams.fromInput({ a: 1, b: true, c: "string", e: [1, 2, 3] })
286
+ * const result = UrlParams.toRecord(urlParams)
287
+ *
288
+ * assert.deepStrictEqual(
289
+ * result,
290
+ * { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] }
291
+ * )
292
+ *
293
+ * @since 1.0.0
294
+ * @category conversions
295
+ */
296
+ export const toRecord = (self: UrlParams): Record<string, string | Arr.NonEmptyArray<string>> => {
297
+ const out: Record<string, string | Arr.NonEmptyArray<string>> = {}
298
+ for (const [k, value] of self) {
299
+ const curr = out[k]
300
+ if (curr === undefined) {
301
+ out[k] = value
302
+ } else if (typeof curr === "string") {
303
+ out[k] = [curr, value]
304
+ } else {
305
+ curr.push(value)
306
+ }
307
+ }
308
+ return out
309
+ }
310
+
277
311
  /**
278
312
  * @since 1.0.0
279
313
  * @category schema
@@ -1,3 +1,4 @@
1
+ import * as Cause from "effect/Cause"
1
2
  import * as Context from "effect/Context"
2
3
  import * as Effect from "effect/Effect"
3
4
  import type * as Fiber from "effect/Fiber"
@@ -11,6 +12,7 @@ import * as Predicate from "effect/Predicate"
11
12
  import * as Ref from "effect/Ref"
12
13
  import * as Schedule from "effect/Schedule"
13
14
  import * as Scope from "effect/Scope"
15
+ import type { NoInfer } from "effect/Types"
14
16
  import * as Cookies from "../Cookies.js"
15
17
  import * as Headers from "../Headers.js"
16
18
  import type * as Client from "../HttpClient.js"
@@ -565,6 +567,7 @@ export const retry: {
565
567
  export const retryTransient: {
566
568
  <B, E, R1 = never>(
567
569
  options: {
570
+ readonly while?: Predicate.Predicate<NoInfer<E>>
568
571
  readonly schedule?: Schedule.Schedule<B, NoInfer<E>, R1>
569
572
  readonly times?: number
570
573
  } | Schedule.Schedule<B, NoInfer<E>, R1>
@@ -572,6 +575,7 @@ export const retryTransient: {
572
575
  <E, R, B, R1 = never>(
573
576
  self: Client.HttpClient<E, R>,
574
577
  options: {
578
+ readonly while?: Predicate.Predicate<NoInfer<E>>
575
579
  readonly schedule?: Schedule.Schedule<B, NoInfer<E>, R1>
576
580
  readonly times?: number
577
581
  } | Schedule.Schedule<B, NoInfer<E>, R1>
@@ -581,6 +585,7 @@ export const retryTransient: {
581
585
  <E extends E0, E0, R, B, R1 = never>(
582
586
  self: Client.HttpClient<E, R>,
583
587
  options: {
588
+ readonly while?: Predicate.Predicate<NoInfer<E>>
584
589
  readonly schedule?: Schedule.Schedule<B, NoInfer<E>, R1>
585
590
  readonly times?: number
586
591
  } | Schedule.Schedule<B, NoInfer<E>, R1>
@@ -588,16 +593,23 @@ export const retryTransient: {
588
593
  transformResponse(
589
594
  self,
590
595
  Effect.retry({
591
- while: (error) =>
592
- Error.isHttpClientError(error) &&
593
- ((error._tag === "RequestError" && error.reason === "Transport") ||
594
- (error._tag === "ResponseError" && error.response.status >= 429)),
596
+ while: Schedule.ScheduleTypeId in options || options.while === undefined
597
+ ? isTransientError
598
+ : Predicate.or(isTransientError, options.while),
595
599
  schedule: Schedule.ScheduleTypeId in options ? options : options.schedule,
596
600
  times: Schedule.ScheduleTypeId in options ? undefined : options.times
597
601
  })
598
602
  )
599
603
  )
600
604
 
605
+ const isTransientError = (error: unknown) =>
606
+ Predicate.hasProperty(error, Cause.TimeoutExceptionTypeId) || isTransientHttpError(error)
607
+
608
+ const isTransientHttpError = (error: unknown) =>
609
+ Error.isHttpClientError(error) &&
610
+ ((error._tag === "RequestError" && error.reason === "Transport") ||
611
+ (error._tag === "ResponseError" && error.response.status >= 429))
612
+
601
613
  /** @internal */
602
614
  export const tap = dual<
603
615
  <_, E2, R2>(