effect 4.0.0-beta.80 → 4.0.0-beta.82

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 (48) hide show
  1. package/dist/Config.d.ts.map +1 -1
  2. package/dist/Config.js +5 -2
  3. package/dist/Config.js.map +1 -1
  4. package/dist/Schema.d.ts +1 -1
  5. package/dist/Schema.d.ts.map +1 -1
  6. package/dist/unstable/encoding/Sse.d.ts +18 -14
  7. package/dist/unstable/encoding/Sse.d.ts.map +1 -1
  8. package/dist/unstable/encoding/Sse.js +7 -4
  9. package/dist/unstable/encoding/Sse.js.map +1 -1
  10. package/dist/unstable/httpapi/HttpApi.d.ts +1 -1
  11. package/dist/unstable/httpapi/HttpApi.d.ts.map +1 -1
  12. package/dist/unstable/httpapi/HttpApi.js +1 -0
  13. package/dist/unstable/httpapi/HttpApi.js.map +1 -1
  14. package/dist/unstable/httpapi/HttpApiBuilder.d.ts.map +1 -1
  15. package/dist/unstable/httpapi/HttpApiBuilder.js +92 -11
  16. package/dist/unstable/httpapi/HttpApiBuilder.js.map +1 -1
  17. package/dist/unstable/httpapi/HttpApiClient.d.ts +6 -1
  18. package/dist/unstable/httpapi/HttpApiClient.d.ts.map +1 -1
  19. package/dist/unstable/httpapi/HttpApiClient.js +114 -3
  20. package/dist/unstable/httpapi/HttpApiClient.js.map +1 -1
  21. package/dist/unstable/httpapi/HttpApiEndpoint.d.ts +60 -50
  22. package/dist/unstable/httpapi/HttpApiEndpoint.d.ts.map +1 -1
  23. package/dist/unstable/httpapi/HttpApiEndpoint.js +116 -5
  24. package/dist/unstable/httpapi/HttpApiEndpoint.js.map +1 -1
  25. package/dist/unstable/httpapi/HttpApiGroup.d.ts +1 -1
  26. package/dist/unstable/httpapi/HttpApiGroup.d.ts.map +1 -1
  27. package/dist/unstable/httpapi/HttpApiSchema.d.ts +116 -2
  28. package/dist/unstable/httpapi/HttpApiSchema.d.ts.map +1 -1
  29. package/dist/unstable/httpapi/HttpApiSchema.js +75 -5
  30. package/dist/unstable/httpapi/HttpApiSchema.js.map +1 -1
  31. package/dist/unstable/httpapi/OpenApi.d.ts +15 -0
  32. package/dist/unstable/httpapi/OpenApi.d.ts.map +1 -1
  33. package/dist/unstable/httpapi/OpenApi.js +82 -13
  34. package/dist/unstable/httpapi/OpenApi.js.map +1 -1
  35. package/dist/unstable/reactivity/AtomHttpApi.d.ts +2 -2
  36. package/dist/unstable/reactivity/AtomHttpApi.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/Config.ts +5 -2
  39. package/src/Schema.ts +1 -1
  40. package/src/unstable/encoding/Sse.ts +34 -20
  41. package/src/unstable/httpapi/HttpApi.ts +2 -1
  42. package/src/unstable/httpapi/HttpApiBuilder.ts +148 -3
  43. package/src/unstable/httpapi/HttpApiClient.ts +196 -5
  44. package/src/unstable/httpapi/HttpApiEndpoint.ts +209 -18
  45. package/src/unstable/httpapi/HttpApiGroup.ts +1 -1
  46. package/src/unstable/httpapi/HttpApiSchema.ts +249 -5
  47. package/src/unstable/httpapi/OpenApi.ts +130 -17
  48. package/src/unstable/reactivity/AtomHttpApi.ts +2 -2
@@ -21,8 +21,10 @@ import * as Schema from "../../Schema.ts"
21
21
  import * as SchemaAST from "../../SchemaAST.ts"
22
22
  import * as SchemaIssue from "../../SchemaIssue.ts"
23
23
  import * as SchemaTransformation from "../../SchemaTransformation.ts"
24
+ import * as Stream from "../../Stream.ts"
24
25
  import type { Simplify } from "../../Types.ts"
25
26
  import * as UndefinedOr from "../../UndefinedOr.ts"
27
+ import * as Sse from "../encoding/Sse.ts"
26
28
  import * as HttpBody from "../http/HttpBody.ts"
27
29
  import * as HttpClient from "../http/HttpClient.ts"
28
30
  import * as HttpClientError from "../http/HttpClientError.ts"
@@ -68,6 +70,30 @@ export type ForApi<Api extends HttpApi.Any, E = never, R = never> = Api extends
68
70
  HttpApi.HttpApi<infer _Id, infer Groups> ? Client<Groups, E, R> :
69
71
  never
70
72
 
73
+ type SuccessType<S> = S extends HttpApiSchema.StreamSse<
74
+ infer _Events,
75
+ infer _Error,
76
+ infer _Value
77
+ > ? Stream.Stream<
78
+ _Value,
79
+ _Error["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry,
80
+ never
81
+ >
82
+ : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream<Uint8Array, HttpClientError.HttpClientError, never>
83
+ : S extends Schema.Top ? S["Type"]
84
+ : never
85
+
86
+ type SuccessDecodingServices<S> = S extends HttpApiSchema.StreamSse<
87
+ infer _Events,
88
+ infer _Error,
89
+ infer _Value
90
+ > ?
91
+ | _Events["DecodingServices"]
92
+ | _Error["DecodingServices"]
93
+ : S extends HttpApiSchema.StreamUint8Array ? never
94
+ : S extends Schema.Top ? S["DecodingServices"]
95
+ : never
96
+
71
97
  /**
72
98
  * Helper types used to describe generated HTTP API clients, including endpoint
73
99
  * methods, response modes, and grouped client shapes.
@@ -135,7 +161,7 @@ export declare namespace Client {
135
161
  ] ? <Mode extends ResponseMode = ResponseMode>(
136
162
  request: Simplify<HttpApiEndpoint.ClientRequest<_Params, _Query, _Payload, _Headers, Mode>>
137
163
  ) => Effect.Effect<
138
- Response<_Success["Type"], Mode>,
164
+ Response<SuccessType<_Success>, Mode>,
139
165
  | HttpApiMiddleware.Error<_Middleware>
140
166
  | HttpApiMiddleware.ClientError<_Middleware>
141
167
  | E
@@ -146,7 +172,10 @@ export declare namespace Client {
146
172
  | _Query["EncodingServices"]
147
173
  | _Payload["EncodingServices"]
148
174
  | _Headers["EncodingServices"]
149
- | ([Mode] extends ["response-only"] ? never : _Success["DecodingServices"] | _Error["DecodingServices"])
175
+ | ([Mode] extends ["response-only"] ? never
176
+ :
177
+ | SuccessDecodingServices<_Success>
178
+ | _Error["DecodingServices"])
150
179
  > :
151
180
  never
152
181
 
@@ -312,9 +341,25 @@ export const makeClient = <ApiId extends string, Groups extends HttpApiGroup.Any
312
341
  Effect.fail
313
342
  )
314
343
  })
315
- successes.forEach((schemas, status) => {
316
- decodeMap[status] = schemasToResponse(schemas)
317
- })
344
+
345
+ const successAlternatives = new Map<number, Array<ResponseAlternative>>()
346
+ for (const [status, schemas] of successes.entries()) {
347
+ const grouped = groupSchemasByContentType(schemas)
348
+ for (const [contentType, schemas] of grouped.entries()) {
349
+ addResponseAlternative(successAlternatives, status, contentType, schemasToResponse(schemas))
350
+ }
351
+ }
352
+ for (const streamSuccess of getStreamSuccessSchemas(endpoint)) {
353
+ addResponseAlternative(
354
+ successAlternatives,
355
+ HttpApiSchema.getStatusStream(streamSuccess),
356
+ streamSuccess.contentType,
357
+ streamToResponse(streamSuccess)
358
+ )
359
+ }
360
+ for (const [status, alternatives] of successAlternatives.entries()) {
361
+ decodeMap[status] = makeResponseDecoder(alternatives)
362
+ }
318
363
 
319
364
  // encoders
320
365
  const encodeParams = UndefinedOr.map(endpoint.params, Schema.encodeUnknownEffect)
@@ -648,6 +693,152 @@ function schemasToResponse(schemas: readonly [Schema.Top, ...Array<Schema.Top>])
648
693
  return (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap(response.arrayBuffer, decode)
649
694
  }
650
695
 
696
+ type ResponseDecoder = (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<unknown, unknown, unknown>
697
+
698
+ interface ResponseAlternative {
699
+ readonly contentType: string
700
+ readonly decode: ResponseDecoder
701
+ }
702
+
703
+ function addResponseAlternative(
704
+ map: Map<number, Array<ResponseAlternative>>,
705
+ status: number,
706
+ contentType: string,
707
+ decode: ResponseDecoder
708
+ ) {
709
+ const normalizedContentType = normalizeContentType(contentType)
710
+ const alternatives = map.get(status)
711
+ if (alternatives === undefined) {
712
+ map.set(status, [{ contentType: normalizedContentType, decode }])
713
+ } else {
714
+ alternatives.push({ contentType: normalizedContentType, decode })
715
+ }
716
+ }
717
+
718
+ function makeResponseDecoder(alternatives: ReadonlyArray<ResponseAlternative>): ResponseDecoder {
719
+ const first = alternatives[0]
720
+ if (alternatives.length === 1 && first !== undefined) {
721
+ return first.decode
722
+ }
723
+ return (response) => {
724
+ const contentType = normalizeContentType(response.headers["content-type"] ?? "")
725
+ const alternative = alternatives.find((alternative) => alternative.contentType === contentType)
726
+ return alternative === undefined
727
+ ? failUnsupportedContentType(response, contentType, alternatives)
728
+ : alternative.decode(response)
729
+ }
730
+ }
731
+
732
+ function groupSchemasByContentType(
733
+ schemas: Arr.NonEmptyReadonlyArray<Schema.Top>
734
+ ): Map<string, Arr.NonEmptyReadonlyArray<Schema.Top>> {
735
+ const grouped = new Map<string, [Schema.Top, ...Array<Schema.Top>]>()
736
+ for (const schema of schemas) {
737
+ const contentType = HttpApiSchema.getResponseEncoding(schema.ast).contentType
738
+ const existing = grouped.get(contentType)
739
+ if (existing === undefined) {
740
+ grouped.set(contentType, [schema])
741
+ } else {
742
+ existing.push(schema)
743
+ }
744
+ }
745
+ return grouped
746
+ }
747
+
748
+ function normalizeContentType(contentType: string): string {
749
+ const normalized = contentType.toLowerCase().trim()
750
+ const index = normalized.indexOf(";")
751
+ return index === -1 ? normalized : normalized.slice(0, index).trim()
752
+ }
753
+
754
+ function failUnsupportedContentType(
755
+ response: HttpClientResponse.HttpClientResponse,
756
+ contentType: string,
757
+ alternatives: ReadonlyArray<ResponseAlternative>
758
+ ) {
759
+ const expected = Array.from(new Set(alternatives.map((alternative) => alternative.contentType))).join(", ")
760
+ return Effect.fail(
761
+ new HttpClientError.HttpClientError({
762
+ reason: new HttpClientError.DecodeError({
763
+ request: response.request,
764
+ response,
765
+ description: `Unsupported response content-type for status ${response.status}: ${
766
+ contentType || "<missing>"
767
+ }. Expected one of: ${expected}`
768
+ })
769
+ })
770
+ )
771
+ }
772
+
773
+ const reservedStreamFailureEvent = "effect/httpapi/stream/failure"
774
+
775
+ function getStreamSuccessSchemas(endpoint: HttpApiEndpoint.AnyWithProps): Array<HttpApiSchema.StreamSchema> {
776
+ const schemas: Array<HttpApiSchema.StreamSchema> = []
777
+ for (const schema of endpoint.success) {
778
+ if (HttpApiSchema.isStreamSchema(schema)) {
779
+ schemas.push(schema)
780
+ }
781
+ }
782
+ return schemas
783
+ }
784
+
785
+ function streamToResponse(streamSchema: HttpApiSchema.StreamSchema) {
786
+ return (response: HttpClientResponse.HttpClientResponse) =>
787
+ Effect.map(Effect.context<never>(), (context) =>
788
+ Stream.provideContext(
789
+ HttpApiSchema.isStreamUint8Array(streamSchema) ?
790
+ response.stream :
791
+ decodeSseStream(response.stream, streamSchema),
792
+ context as Context.Context<unknown>
793
+ ))
794
+ }
795
+
796
+ function decodeSseStream(
797
+ stream: Stream.Stream<Uint8Array, HttpClientError.HttpClientError>,
798
+ declaration: HttpApiSchema.StreamSse<Sse.EventCodec, Schema.Top, unknown>
799
+ ): Stream.Stream<unknown, unknown, unknown> {
800
+ const Event = Schema.Union([
801
+ declaration.events,
802
+ Schema.Struct({
803
+ event: Schema.Literal(reservedStreamFailureEvent),
804
+ data: Schema.fromJsonString(Schema.toCodecJson(Schema.Cause(declaration.error, Schema.Defect())))
805
+ })
806
+ ])
807
+ const events = Stream.transformPull(
808
+ stream.pipe(
809
+ Stream.decodeText,
810
+ Stream.pipeThroughChannel(Sse.decodeSchema(Event))
811
+ ),
812
+ (pull) =>
813
+ Effect.sync(() => {
814
+ let failureCause: Cause.Cause<unknown> | undefined = undefined
815
+ return Effect.suspend(() => {
816
+ if (failureCause) {
817
+ return Effect.failCause(failureCause)
818
+ }
819
+ return Effect.flatMap(pull, (events) => {
820
+ for (let i = 0; i < events.length; i++) {
821
+ const event = events[i]
822
+ if (event.event === reservedStreamFailureEvent) {
823
+ if (i === 0) {
824
+ return Effect.failCause(event.data)
825
+ }
826
+ failureCause = event.data
827
+ events = events.slice(0, i) as any
828
+ break
829
+ }
830
+ }
831
+ return Effect.succeed(events)
832
+ })
833
+ })
834
+ })
835
+ )
836
+ if (declaration.sseMode === "data") {
837
+ return Stream.map(events, (event) => event.data)
838
+ }
839
+ return events
840
+ }
841
+
651
842
  const ArrayBuffer = Schema.instanceOf(globalThis.ArrayBuffer, {
652
843
  expected: "ArrayBuffer"
653
844
  })
@@ -19,6 +19,7 @@ import { identity } from "../../Function.ts"
19
19
  import { type Pipeable, pipeArguments } from "../../Pipeable.ts"
20
20
  import * as Predicate from "../../Predicate.ts"
21
21
  import * as Schema from "../../Schema.ts"
22
+ import * as AST from "../../SchemaAST.ts"
22
23
  import type * as Stream from "../../Stream.ts"
23
24
  import type * as Types from "../../Types.ts"
24
25
  import type { HttpMethod } from "../http/HttpMethod.ts"
@@ -53,6 +54,48 @@ export type PayloadMap = ReadonlyMap<string, {
53
54
  readonly schemas: [Schema.Top, ...Array<Schema.Top>]
54
55
  }>
55
56
 
57
+ type SuccessType<S> = S extends HttpApiSchema.StreamSse<
58
+ infer _Events,
59
+ infer _Error,
60
+ infer _Value
61
+ > ? Stream.Stream<_Value, _Error["Type"], never>
62
+ : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream<Uint8Array, unknown, never>
63
+ : S extends Schema.Top ? S["Type"]
64
+ : never
65
+
66
+ type SuccessEncodingServices<S> = S extends HttpApiSchema.StreamSse<
67
+ infer _Events,
68
+ infer _Error,
69
+ infer _Value
70
+ > ? _Events["EncodingServices"] | _Error["EncodingServices"]
71
+ : S extends HttpApiSchema.StreamUint8Array ? never
72
+ : S extends Schema.Top ? S["EncodingServices"]
73
+ : never
74
+
75
+ type SuccessDecodingServices<S> = S extends HttpApiSchema.StreamSse<
76
+ infer _Events,
77
+ infer _Error,
78
+ infer _Value
79
+ > ? _Events["DecodingServices"] | _Error["DecodingServices"]
80
+ : S extends HttpApiSchema.StreamUint8Array ? never
81
+ : S extends Schema.Top ? S["DecodingServices"]
82
+ : never
83
+
84
+ type ExtractSuccessOrArray<S extends SuccessConstraint> = S extends ReadonlyArray<Schema.Top> ? S[number] : S
85
+
86
+ type ExtractBufferedSuccess<S extends SuccessConstraint> = Exclude<
87
+ Extract<ExtractSuccessOrArray<S>, Schema.Top>,
88
+ HttpApiSchema.StreamSchema
89
+ >
90
+
91
+ type ExtractStreamSuccess<S extends SuccessConstraint> = ExtractSuccessOrArray<S> extends infer Success ?
92
+ Success extends HttpApiSchema.StreamSchema ? Success : never
93
+ : never
94
+
95
+ type JsonSuccessOrArray<S extends SuccessConstraint> = [ExtractBufferedSuccess<S>] extends [never] ?
96
+ ExtractStreamSuccess<S>
97
+ : Json<ExtractBufferedSuccess<S>> | ExtractStreamSuccess<S>
98
+
56
99
  /**
57
100
  * Represents an API endpoint. An API endpoint is mapped to a single route on
58
101
  * the underlying `HttpRouter`.
@@ -587,7 +630,7 @@ export type ServerServices<Endpoint> = Endpoint extends HttpApiEndpoint<
587
630
  | _Query["DecodingServices"]
588
631
  | _Payload["DecodingServices"]
589
632
  | _Headers["DecodingServices"]
590
- | _Success["EncodingServices"]
633
+ | SuccessEncodingServices<_Success>
591
634
  | _Error["EncodingServices"]
592
635
  | HttpApiMiddleware.ErrorServicesEncode<_M>
593
636
  : never
@@ -616,7 +659,7 @@ export type ClientServices<Endpoint> = Endpoint extends HttpApiEndpoint<
616
659
  | _Query["EncodingServices"]
617
660
  | _Payload["EncodingServices"]
618
661
  | _Headers["EncodingServices"]
619
- | _Success["DecodingServices"]
662
+ | SuccessDecodingServices<_Success>
620
663
  | _Error["DecodingServices"]
621
664
  : never
622
665
 
@@ -672,7 +715,7 @@ export type ErrorServicesDecode<Endpoint> = Endpoint extends HttpApiEndpoint<
672
715
  */
673
716
  export type Handler<Endpoint extends Any, E, R> = (
674
717
  request: Types.Simplify<Request<Endpoint>>
675
- ) => Effect<Endpoint["~Success"]["Type"] | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R>
718
+ ) => Effect<SuccessType<Endpoint["~Success"]> | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R>
676
719
 
677
720
  /**
678
721
  * The raw server handler for an endpoint, receiving a request shape without a
@@ -683,7 +726,7 @@ export type Handler<Endpoint extends Any, E, R> = (
683
726
  */
684
727
  export type HandlerRaw<Endpoint extends Any, E, R> = (
685
728
  request: Types.Simplify<RequestRaw<Endpoint>>
686
- ) => Effect<Endpoint["~Success"]["Type"] | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R>
729
+ ) => Effect<SuccessType<Endpoint["~Success"]> | HttpServerResponse, Endpoint["~Error"]["Type"] | E, R>
687
730
 
688
731
  /**
689
732
  * Selects the endpoint with the specified name from a union of endpoints.
@@ -736,7 +779,7 @@ export type HandlerRawWithName<Endpoints extends Any, Name extends string, E, R>
736
779
  */
737
780
  export type SuccessWithName<Endpoints extends Any, Name extends string> = Success<
738
781
  WithName<Endpoints, Name>
739
- >["Type"]
782
+ > extends infer S ? SuccessType<S> : never
740
783
 
741
784
  /**
742
785
  * Computes the full error value union for the endpoint with the specified name in
@@ -1024,7 +1067,7 @@ export type PayloadConstraint<Method extends HttpMethod> = Method extends HttpMe
1024
1067
  string,
1025
1068
  Schema.Encoder<string | ReadonlyArray<string> | undefined, unknown>
1026
1069
  > :
1027
- SuccessConstraint
1070
+ Schema.Top | ReadonlyArray<Schema.Top>
1028
1071
 
1029
1072
  /**
1030
1073
  * Payload constraint used when automatic codecs are enabled: no-body methods
@@ -1056,6 +1099,13 @@ export type SuccessConstraint = Schema.Top | ReadonlyArray<Schema.Top>
1056
1099
  */
1057
1100
  export type ErrorConstraint = Schema.Top | ReadonlyArray<Schema.Top>
1058
1101
 
1102
+ type ErrorNoStream<S extends ErrorConstraint> = [
1103
+ Extract<
1104
+ S extends ReadonlyArray<Schema.Top> ? S[number] : S,
1105
+ HttpApiSchema.StreamSchema
1106
+ >
1107
+ ] extends [never] ? S : never
1108
+
1059
1109
  /**
1060
1110
  * Creates endpoint constructors for a specific HTTP method. The resulting
1061
1111
  * constructor builds an `HttpApiEndpoint` from a name, path, and optional request
@@ -1073,7 +1123,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
1073
1123
  Query extends Schema.Top | Schema.Struct.Fields = never,
1074
1124
  Payload extends PayloadConstraintCodecs<Method> = never,
1075
1125
  Headers extends Schema.Top | Schema.Struct.Fields = never,
1076
- const Success extends Schema.Top | ReadonlyArray<Schema.Top> = HttpApiSchema.NoContent,
1126
+ const Success extends SuccessConstraint = HttpApiSchema.NoContent,
1077
1127
  const Error extends Schema.Top | ReadonlyArray<Schema.Top> = never
1078
1128
  >(
1079
1129
  name: Name,
@@ -1085,7 +1135,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
1085
1135
  readonly headers?: Headers | undefined
1086
1136
  readonly payload?: Payload | undefined
1087
1137
  readonly success?: Success | undefined
1088
- readonly error?: Error | undefined
1138
+ readonly error?: (Error & ErrorNoStream<Types.NoInfer<Error>>) | undefined
1089
1139
  }
1090
1140
  ): HttpApiEndpoint<
1091
1141
  Name,
@@ -1096,7 +1146,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
1096
1146
  Method extends HttpMethod.WithBody ? Json<ExtractSchemaOrArray<Payload>>
1097
1147
  : StringTree<ExtractSchemaOrArray<Payload>>,
1098
1148
  StringTree<Headers extends Schema.Struct.Fields ? Schema.Struct<Headers> : Headers>,
1099
- Json<Success extends ReadonlyArray<Schema.Top> ? Success[number] : Success>,
1149
+ JsonSuccessOrArray<Success>,
1100
1150
  Json<Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error>
1101
1151
  >
1102
1152
  <
@@ -1118,7 +1168,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
1118
1168
  readonly headers?: Headers | undefined
1119
1169
  readonly payload?: Payload | undefined
1120
1170
  readonly success?: Success | undefined
1121
- readonly error?: Error | undefined
1171
+ readonly error?: (Error & ErrorNoStream<Types.NoInfer<Error>>) | undefined
1122
1172
  }
1123
1173
  ): HttpApiEndpoint<
1124
1174
  Name,
@@ -1128,7 +1178,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
1128
1178
  Query extends Schema.Struct.Fields ? Schema.Struct<Query> : Query,
1129
1179
  ExtractSchemaOrArray<Payload>,
1130
1180
  ExtractSchemaOrArray<Headers>,
1131
- Success extends ReadonlyArray<Schema.Top> ? Success[number] : Success,
1181
+ ExtractSuccessOrArray<Success>,
1132
1182
  Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error
1133
1183
  >
1134
1184
  } =>
@@ -1151,7 +1201,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
1151
1201
  readonly headers?: Headers | undefined
1152
1202
  readonly payload?: Payload | undefined
1153
1203
  readonly success?: Success | undefined
1154
- readonly error?: Error | undefined
1204
+ readonly error?: (Error & ErrorNoStream<Types.NoInfer<Error>>) | undefined
1155
1205
  }
1156
1206
  ): HttpApiEndpoint<
1157
1207
  Name,
@@ -1163,7 +1213,7 @@ export const make = <Method extends HttpMethod>(method: Method): {
1163
1213
  : Payload extends ReadonlyArray<Schema.Top> ? Payload[number]
1164
1214
  : Payload,
1165
1215
  Headers extends Schema.Struct.Fields ? Schema.Struct<Headers> : Headers,
1166
- Success extends ReadonlyArray<Schema.Top> ? Success[number] : Success,
1216
+ ExtractSuccessOrArray<Success>,
1167
1217
  Error extends ReadonlyArray<Schema.Top> ? Error[number] : Error
1168
1218
  > => {
1169
1219
  const disableCodecs = options?.disableCodecs ?? false
@@ -1176,8 +1226,8 @@ export const make = <Method extends HttpMethod>(method: Method): {
1176
1226
  query: ensureStruct(options?.query, transformStringTree),
1177
1227
  headers: ensureStruct(options?.headers, transformStringTree),
1178
1228
  payload: getPayload(options?.payload, method, disableCodecs),
1179
- success: getResponse(options?.success, disableCodecs),
1180
- error: getResponse(options?.error, disableCodecs),
1229
+ success: getSuccessResponse(options?.success, method, disableCodecs),
1230
+ error: getErrorResponse(options?.error, disableCodecs),
1181
1231
  annotations: Context.empty(),
1182
1232
  middlewares: new Set()
1183
1233
  })
@@ -1257,13 +1307,154 @@ function getPayload(
1257
1307
  return result
1258
1308
  }
1259
1309
 
1260
- function getResponse(
1310
+ const reservedStreamFailureEvent = "effect/httpapi/stream/failure"
1311
+
1312
+ function getSuccessResponse(
1261
1313
  success: Schema.Top | ReadonlyArray<Schema.Top> | undefined,
1314
+ method: HttpMethod,
1262
1315
  disableCodecs: boolean
1263
1316
  ): Set<Schema.Top> {
1264
1317
  if (success === undefined) return new Set()
1265
- const arr = Arr.ensure(success)
1266
- return new Set(disableCodecs ? arr : arr.map(transformResponse))
1318
+ const schemas = Arr.ensure(success)
1319
+ validateSuccessResponse(schemas, method)
1320
+ return new Set(
1321
+ disableCodecs ?
1322
+ schemas :
1323
+ schemas.map((schema) => HttpApiSchema.isStreamSchema(schema) ? schema : transformResponse(schema))
1324
+ )
1325
+ }
1326
+
1327
+ function getErrorResponse(
1328
+ error: Schema.Top | ReadonlyArray<Schema.Top> | undefined,
1329
+ disableCodecs: boolean
1330
+ ): Set<Schema.Top> {
1331
+ if (error === undefined) return new Set()
1332
+ const schemas = Arr.ensure(error)
1333
+ for (const schema of schemas) {
1334
+ if (HttpApiSchema.isStreamSchema(schema)) {
1335
+ throw new Error("Streaming schemas are not supported in error responses")
1336
+ }
1337
+ }
1338
+ return new Set(disableCodecs ? schemas : schemas.map(transformResponse))
1339
+ }
1340
+
1341
+ function validateSuccessResponse(schemas: ReadonlyArray<Schema.Top>, method: HttpMethod) {
1342
+ const statuses = new Map<number, {
1343
+ readonly stream?: HttpApiSchema.StreamSchema | undefined
1344
+ bufferedContentTypes: Set<string>
1345
+ noContent: boolean
1346
+ }>()
1347
+
1348
+ for (const schema of schemas) {
1349
+ if (HttpApiSchema.isStreamSchema(schema)) {
1350
+ validateStreamSuccess(schema, method)
1351
+ const status = HttpApiSchema.getStatusStream(schema)
1352
+ const entry = getStatusEntry(statuses, status)
1353
+ if (entry.stream !== undefined) {
1354
+ throw new Error(`Multiple streaming success responses for status: ${status}`)
1355
+ }
1356
+ if (entry.noContent) {
1357
+ throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`)
1358
+ }
1359
+ if (entry.bufferedContentTypes.has(normalizeResponseContentType(schema.contentType))) {
1360
+ throw new Error(
1361
+ `Cannot combine buffered and streaming success responses for status ${status} and content-type: ${schema.contentType}`
1362
+ )
1363
+ }
1364
+ statuses.set(status, { ...entry, stream: schema })
1365
+ } else {
1366
+ const status = HttpApiSchema.getStatusSuccess(schema.ast)
1367
+ const entry = getStatusEntry(statuses, status)
1368
+ const noContent = HttpApiSchema.isNoContent(schema.ast)
1369
+ if (entry.stream !== undefined) {
1370
+ if (noContent) {
1371
+ throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`)
1372
+ }
1373
+ const encoding = HttpApiSchema.getResponseEncoding(schema.ast)
1374
+ if (
1375
+ normalizeResponseContentType(encoding.contentType) === normalizeResponseContentType(entry.stream.contentType)
1376
+ ) {
1377
+ throw new Error(
1378
+ `Cannot combine buffered and streaming success responses for status ${status} and content-type: ${encoding.contentType}`
1379
+ )
1380
+ }
1381
+ }
1382
+ if (!noContent) {
1383
+ entry.bufferedContentTypes.add(
1384
+ normalizeResponseContentType(HttpApiSchema.getResponseEncoding(schema.ast).contentType)
1385
+ )
1386
+ }
1387
+ entry.noContent = entry.noContent || noContent
1388
+ }
1389
+ }
1390
+ }
1391
+
1392
+ function normalizeResponseContentType(contentType: string): string {
1393
+ const normalized = contentType.toLowerCase().trim()
1394
+ const index = normalized.indexOf(";")
1395
+ return index === -1 ? normalized : normalized.slice(0, index).trim()
1396
+ }
1397
+
1398
+ function getStatusEntry(
1399
+ statuses: Map<number, {
1400
+ readonly stream?: HttpApiSchema.StreamSchema | undefined
1401
+ bufferedContentTypes: Set<string>
1402
+ noContent: boolean
1403
+ }>,
1404
+ status: number
1405
+ ) {
1406
+ let entry = statuses.get(status)
1407
+ if (entry === undefined) {
1408
+ entry = { bufferedContentTypes: new Set(), noContent: false }
1409
+ statuses.set(status, entry)
1410
+ }
1411
+ return entry
1412
+ }
1413
+
1414
+ function validateStreamSuccess(schema: HttpApiSchema.StreamSchema, method: HttpMethod) {
1415
+ if (method === "HEAD") {
1416
+ throw new Error("HEAD endpoints cannot declare streaming success responses")
1417
+ }
1418
+ if (HttpApiSchema.isStreamSse(schema) && hasReservedSseEventName(schema.events.ast)) {
1419
+ throw new Error(`SSE event name is reserved: ${reservedStreamFailureEvent}`)
1420
+ }
1421
+ }
1422
+
1423
+ function hasReservedSseEventName(ast: AST.AST): boolean {
1424
+ return hasReservedEventName(AST.toEncoded(ast), new Set())
1425
+ }
1426
+
1427
+ function hasReservedEventName(ast: AST.AST, seen: Set<AST.AST>): boolean {
1428
+ if (seen.has(ast)) return false
1429
+ seen.add(ast)
1430
+ if (AST.isUnion(ast)) {
1431
+ return ast.types.some((type) => hasReservedEventName(type, seen))
1432
+ }
1433
+ if (AST.isSuspend(ast)) {
1434
+ return hasReservedEventName(ast.thunk(), seen)
1435
+ }
1436
+ if (!AST.isObjects(ast)) return false
1437
+ const event = ast.propertySignatures.find((ps) => ps.name === "event")
1438
+ return event !== undefined && hasReservedEventLiteral(event.type, seen)
1439
+ }
1440
+
1441
+ function hasReservedEventLiteral(ast: AST.AST, seen: Set<AST.AST>): boolean {
1442
+ if (seen.has(ast)) return false
1443
+ seen.add(ast)
1444
+ const encoded = AST.toEncoded(ast)
1445
+ if (encoded !== ast) {
1446
+ return hasReservedEventLiteral(encoded, seen)
1447
+ }
1448
+ if (AST.isLiteral(ast)) {
1449
+ return ast.literal === reservedStreamFailureEvent
1450
+ }
1451
+ if (AST.isUnion(ast)) {
1452
+ return ast.types.some((type) => hasReservedEventLiteral(type, seen))
1453
+ }
1454
+ if (AST.isSuspend(ast)) {
1455
+ return hasReservedEventLiteral(ast.thunk(), seen)
1456
+ }
1457
+ return false
1267
1458
  }
1268
1459
 
1269
1460
  function transformResponse(schema: Schema.Top): Schema.Top {
@@ -57,7 +57,7 @@ export interface HttpApiGroup<
57
57
  /**
58
58
  * Add an `HttpApiEndpoint` to an `HttpApiGroup`.
59
59
  */
60
- add<A extends NonEmptyReadonlyArray<HttpApiEndpoint.Any>>(
60
+ add<const A extends NonEmptyReadonlyArray<HttpApiEndpoint.Any>>(
61
61
  ...endpoints: A
62
62
  ): HttpApiGroup<Id, Endpoints | A[number], TopLevel>
63
63